<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>디벨뤼팽</title>
    <link>https://infitry.tistory.com/</link>
    <description>.</description>
    <language>ko</language>
    <pubDate>Sat, 16 May 2026 14:27:50 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>infitry</managingEditor>
    <image>
      <title>디벨뤼팽</title>
      <url>https://tistory1.daumcdn.net/tistory/4341861/attach/0b9ea243af5244b984e746f1875b03b2</url>
      <link>https://infitry.tistory.com</link>
    </image>
    <item>
      <title>Hikari CP 모니터링하기</title>
      <link>https://infitry.tistory.com/83</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;상황에 따른 Hikari CP의 커넥션 풀 상태가 궁금해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional 어노테이션에 따라 active 가 되고 언제 다시 idle로 돌아가는지 알아보기 위해 모니터링을 위한 준비를 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 클래스 구조에서 확인해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qb2Ds/btsHPggAKE8/OfRJTOytkZk6sZLhBdsQP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qb2Ds/btsHPggAKE8/OfRJTOytkZk6sZLhBdsQP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qb2Ds/btsHPggAKE8/OfRJTOytkZk6sZLhBdsQP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqb2Ds%2FbtsHPggAKE8%2FOfRJTOytkZk6sZLhBdsQP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;591&quot; height=&quot;440&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 테스트 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 먼저 facade 클래스의 메서드가 실행되면 Thread.sleep으로 많은 일을 처리했다고 가정합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. @Transactional 어노테이션이 걸려있는 서비스의 메서드를 호출합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 과정 사이에 HikariCP 로그를 찍어 Connection pool 상태를 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;자세한 소스코드는 다음 &lt;a title=&quot;github&quot; href=&quot;https://github.com/infitry/laboratory&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github를&lt;/a&gt; 참고해 주세요.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 HikariCP를 모니터링하기 위해 application.yml 에 다음 설정을 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소스코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml&lt;/p&gt;
&lt;pre id=&quot;code_1717670059151&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    hikari:
      register-mbeans: true
      pool-name: hikari&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariPoolMXBean을 사용하기 위해 다음과 같이 빈을 등록해 줍니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package com.infitry.laboratory.config;

import com.zaxxer.hikari.HikariPoolMXBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.management.JMX;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;

@Configuration
public class HikariConfig {
    @Bean
    public HikariPoolMXBean hikariPoolMXBean() throws MalformedObjectNameException {
        MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
        ObjectName objectName = new ObjectName(&quot;com.zaxxer.hikari:type=Pool (hikari)&quot;);
        return JMX.newMBeanProxy(mBeanServer, objectName, HikariPoolMXBean.class);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림의 실제 비즈니스 로직이 동작하는 Facade 클래스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConnectionPoolFacade.java&lt;/p&gt;
&lt;pre id=&quot;code_1717669993971&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.infitry.laboratory.service.transaction.connectionpool;

import com.zaxxer.hikari.HikariPoolMXBean;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class ConnectionPoolFacade {
    private final ConnectionPoolService connectionPoolService;
    private final HikariPoolMXBean hikariPoolMXBean;
    private static final int LATENCY = 3000;

    public void doSomethingBefore() throws Exception {
        printCurrentConnectionPool(&quot;1&quot;);
        sleep();
        printCurrentConnectionPool(&quot;2&quot;);
        connectionPoolService.whenGetConnectionInTransactional();
        printCurrentConnectionPool(&quot;3&quot;);
    }

    public void doSomethingAfter() throws Exception {
        printCurrentConnectionPool(&quot;1&quot;);
        connectionPoolService.whenGetConnectionInTransactional();
        printCurrentConnectionPool(&quot;2&quot;);
        sleep();
        printCurrentConnectionPool(&quot;3&quot;);
    }

    private static void sleep() throws InterruptedException {
        log.info(&quot;start sleep!!!&quot;);
        Thread.sleep(LATENCY);
        log.info(&quot;end sleep!!&quot;);
    }

    private void printCurrentConnectionPool(String id) {
        log.info(&quot;{}, (total={}, active={}, idle={}, waiting={})&quot;, id,
                hikariPoolMXBean.getTotalConnections(), hikariPoolMXBean.getActiveConnections(),
                hikariPoolMXBean.getIdleConnections(), hikariPoolMXBean.getThreadsAwaitingConnection());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConnectionPoolService.java&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package com.infitry.laboratory.service.transaction.connectionpool;

import com.infitry.laboratory.persistence.jpa.MemberRepository;
import com.zaxxer.hikari.HikariPoolMXBean;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ConnectionPoolService {
    private final MemberRepository memberRepository;
    private final HikariPoolMXBean hikariPoolMXBean;

    @Transactional
    public void whenGetConnectionInTransactional() throws Exception {
        printCurrentConnectionPool();
        memberRepository.findAll();
        Thread.sleep(2000);
    }

    private void printCurrentConnectionPool() {
        log.info(&quot;in transaction, (total={}, active={}, idle={}, waiting={})&quot;,
                hikariPoolMXBean.getTotalConnections(), hikariPoolMXBean.getActiveConnections(),
                hikariPoolMXBean.getIdleConnections(), hikariPoolMXBean.getThreadsAwaitingConnection());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 doSomethingBefore()를 실행해 보면?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1KMt0/btsHPZ6aWtf/m2xKwOIVIS7zivkc9YA5YK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1KMt0/btsHPZ6aWtf/m2xKwOIVIS7zivkc9YA5YK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1KMt0/btsHPZ6aWtf/m2xKwOIVIS7zivkc9YA5YK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1KMt0%2FbtsHPZ6aWtf%2Fm2xKwOIVIS7zivkc9YA5YK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1466&quot; height=&quot;338&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 처음엔 idle connection 이 5개입니다.&lt;br /&gt;2. sleep 이후 에도 마찬가지로 active connection 이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 트랜잭션이 실행되는 순간 idle connection 이 4개, active connection이 1개가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 트랜잭션이 종료된 후 idle connection 이 다시 5개, active connection이 1개가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;doSomethingAfter() 도 마찬가지로 실행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;331&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccc6DW/btsHQSd9oya/V5TjQ2KNlhXrcPREu5Mwr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccc6DW/btsHQSd9oya/V5TjQ2KNlhXrcPREu5Mwr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccc6DW/btsHQSd9oya/V5TjQ2KNlhXrcPREu5Mwr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fccc6DW%2FbtsHQSd9oya%2FV5TjQ2KNlhXrcPREu5Mwr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1495&quot; height=&quot;331&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;331&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 결과로 트랜잭션 범위에 대한 중요성을 깨달을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 범위를 넓게 잡고 트랜잭션 범위 안에서 시간이 오래 걸리는 작업이 있다면 ex) API 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hikari CP의 connection을 사용하여 반환하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 커넥션이 모두 소진되어 waiting 이 늘어 갈 것이고 오랜 시간 커넥션을 얻지 못한 요청은 오류가 발생할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 사용자의 요청을 처리하기 위해서는 트랜잭션의 범위를 설정하는데 항상 주의가 필요한 것 같습니다.&lt;/p&gt;</description>
      <category>백엔드/Spring Boot</category>
      <category>@Transactional</category>
      <category>Active</category>
      <category>connection pool</category>
      <category>Hikari CP</category>
      <category>idle</category>
      <category>Transactional</category>
      <author>infitry</author>
      <guid isPermaLink="true">https://infitry.tistory.com/83</guid>
      <comments>https://infitry.tistory.com/83#entry83comment</comments>
      <pubDate>Thu, 6 Jun 2024 20:00:12 +0900</pubDate>
    </item>
    <item>
      <title>자기호출 Self Invocation</title>
      <link>https://infitry.tistory.com/82</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;같은 팀원의 코드리뷰 중 JPA 변경감지를 사용하지 않고 명시적으로 saveAll 메서드를 호출하고 있는 코드를 발견하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경감지를 사용하여 처리하는 게 어떻겠냐고 제안하였고 팀원 분께서는 변경감지가 동작하지 않는다고 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드에는 @Transactional 어노테이션이 걸려있었고 트랜잭션 커밋 시 영속성 컨텍스트가 flush 되기 때문에 저는 동작할 것이라고 예상했는데 말이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원 분께 괜찮다면 제가 한 번 실행해 봐도 되냐고 여쭤본 후 실행해 보니 정말 변경감지가 동작하지 않았습니다! &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속 확인해 보다가 이상한 점을 발견했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원 분이 작성하신 코드는 다음과 같은 구조를 가지고 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716619892178&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class SelfInvocation {
    public String parent() {
    	...
        child();
    }
    
    @Transactional
    public void child() {
    
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에서는 parent() 메서드를 호출하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 @Transactional 어노테이션이 프록시 객체를 통해 해당 로직을 실행하기 때문에 발생하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2196&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTvdUt/btsHBYzJyai/dzW13gU7qeV9GkRCzlkRAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTvdUt/btsHBYzJyai/dzW13gU7qeV9GkRCzlkRAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTvdUt/btsHBYzJyai/dzW13gU7qeV9GkRCzlkRAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTvdUt%2FbtsHBYzJyai%2FdzW13gU7qeV9GkRCzlkRAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2196&quot; height=&quot;310&quot; data-origin-width=&quot;2196&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에서는 다음과 같이 설명하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;같은 클래스 내 호출은 차단된다고 합니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 테스트를 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 코드가 있습니다. Order는 간단한 JPA Entity입니다.&lt;br /&gt;전체 코드는 다음 &lt;a href=&quot;https://github.com/infitry/laboratory&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GITHUB&lt;/a&gt; 에서 확인해 보실 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716621098150&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SelfInvocationService {

    private final OrderRepository orderRepository;

    public OrderDto useSelfInvocation() {
        var savedOrder = createNewOrder();
        child(savedOrder);
        return orderRepository.findById(savedOrder.getId())
                .map(OrderDto::from)
                .orElseThrow();
    }

    @Transactional
    public OrderDto unUseSelfInvocation() {
        var savedOrder = createNewOrder();
        child(savedOrder);
        return orderRepository.findById(savedOrder.getId())
                .map(OrderDto::from)
                .orElseThrow();
    }

    /**
     * Self-invocation 으로 인해 트랜잭션이 적용될 수 없음.
     * 따라서 트랜잭션 커밋이 발생하지 않는다.
     */
    @Transactional
    public void child(Order order) {
        order.setOrderName(&quot;order2&quot;);
    }

    private Order createNewOrder() {
        var order = new Order();
        order.setOrderName(&quot;order1&quot;);
        return orderRepository.save(order);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useSelfInvocation() 메서드와 unUseSelfInvocation() 메서드가 외부로 호출되는 메서드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useSelfInvocation() 메서드 내부에서 호출되는 클래스 내 다른 메서드 child() 에는 @Transactional 어노테이션을 추가하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unUseSelfInvocation() 메서드는 @Transactional 어노테이션이 추가되어 있고 child() 메서드에 @Transactional 어노테이션이 있어도 없어도 동일하게 동작합니다. (상위 메서드의 트랜잭션이 전파되기 때문에..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드에서는 트랜잭션 테스트가 어려워 다음과 같은 컨트롤러를 작성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1716623295323&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/self-invocation&quot;)
@RequiredArgsConstructor
public class SelfInvocationController {
    private final SelfInvocationService selfInvocationService;

    @GetMapping(&quot;/1&quot;)
    public OrderDto useSelfInvocation() {
        return selfInvocationService.useSelfInvocation();
    }

    @GetMapping(&quot;/2&quot;)
    public OrderDto unUseSelfInvocation() {
        return selfInvocationService.unUseSelfInvocation();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 자기 호출을 하고 있는 메서드인 useSelfInvocation()을 실행해보면?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2588&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yhQkO/btsHBntnHv4/kK6I9nn79nDIBj0xMz910K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yhQkO/btsHBntnHv4/kK6I9nn79nDIBj0xMz910K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yhQkO/btsHBntnHv4/kK6I9nn79nDIBj0xMz910K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyhQkO%2FbtsHBntnHv4%2FkK6I9nn79nDIBj0xMz910K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2588&quot; height=&quot;750&quot; data-origin-width=&quot;2588&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;insert 쿼리와 select 쿼리하나만 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명했던 것과 같이 child() 메서드의 @Transactional 은 실행되지 않았고 그로 인해 변경한 orderd의 name 은 &quot;order2&quot;로 변경되지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 자기 호출을 하고 있지 않은 unUseSelfInvocation() 메서드를 실행해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2550&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cviajM/btsHBA0jnI6/ZSz5Qib1gnc03LG32tTS01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cviajM/btsHBA0jnI6/ZSz5Qib1gnc03LG32tTS01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cviajM/btsHBA0jnI6/ZSz5Qib1gnc03LG32tTS01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcviajM%2FbtsHBA0jnI6%2FZSz5Qib1gnc03LG32tTS01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2550&quot; height=&quot;774&quot; data-origin-width=&quot;2550&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 &quot;order2&quot;로 업데이트되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어라 근데 useSelfInvocation()를 실행했을 때는 insert 후 select를 하는데 왜 동일한 로직을 사용하는 unUseSelfInvocation() 메서드에서는 select 쿼리가 실행되지 않았을까요? 동일하게 JPA repository에서 findById() 메서드를 호출하는데 말이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;정답은 영속성 컨텍스트의&amp;nbsp;1차 캐시에 있습니다.&lt;/b&gt;&lt;/u&gt; 영속성 컨텍스트는 하나의 트랜잭션에서 유효하고 useSelfInvocation()는 트랜잭션으로 묶여있지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 unUseSelfInvocation() 메서드는 하나의 트랜잭션으로 묶여있고 영속성 컨텍스트를 사용하기 때문에 DB 조회 이전에 영속성 컨텍스트에서 동일한 엔티티가 있는지 먼저 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 해당 엔티티가 영속성 컨텍스트에 존재한다면 해당 엔티티를 가져오기 때문에 DB 조회가 일어나지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 이제 왜 self invocation으로 인해 트랜잭션이 동작하지 않는지는 알아냈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 해야 해당 현상을 막을 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 생각엔 내부에서 호출되는 메서드는 private으로 관리하고 public 메서드에만 @Transactional 어노테이션을 사용하는 게 좋을 것 같습니다. 그리고 @Transactional 어노테이션을 추가할 때는 항상 신중하게 선택해야 할 것 같습니다.&lt;/p&gt;</description>
      <category>백엔드/Spring</category>
      <category>@Transactional</category>
      <category>aop</category>
      <category>self invocation</category>
      <category>Spring</category>
      <category>spring boot</category>
      <category>자기호출</category>
      <author>infitry</author>
      <guid isPermaLink="true">https://infitry.tistory.com/82</guid>
      <comments>https://infitry.tistory.com/82#entry82comment</comments>
      <pubDate>Sat, 25 May 2024 17:25:05 +0900</pubDate>
    </item>
    <item>
      <title>@Transactional propagation</title>
      <link>https://infitry.tistory.com/81</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;@Trasactional 어노테이션의 propagation에 대해 알아보고 중첩된 트랜잭션을 사용하게 될 때 생기는 문제에 대해 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional 어노테이션의 propagation 속성에는 총 7가지의 속성이 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713677917459&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Propagtaion 속성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;REQUIRED -&lt;/b&gt; &lt;br /&gt;기본 속성으로 부모 트랜잭션이 있다면 기존 트랜잭션을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;REQUIRES_NEW -&lt;/b&gt; &lt;br /&gt;항상 새로운 트랜잭션을 사용합니다. 이미 진행중인 트랜잭션이 있다면 보류하고 해당 트랜잭션을 먼저 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SUPPORTS -&lt;/b&gt;&lt;br /&gt;부모 트랜잭션이 있다면 기존 트랜잭션을 사용하고 없으면 트랜잭션 없이 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NOT_SUPPORTED -&lt;/b&gt;&lt;br /&gt;부모 트랜잭션이 있으나 없으나 트랜잭션 없이 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MANDATORY -&lt;/b&gt;&lt;br /&gt;부모 트랜잭션 내에서 실행되며, 활성화된 트랜잭션이 없을 경우 Exception이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NESTED -&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;부모 트랜잭션이 커밋될 때 같이 커밋, 롤백이 부모 트랜잭션에 영향을 미치지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NEVER -&lt;/b&gt;&lt;br /&gt;트랜잭션 없이 실행되며 부모 트랜잭션이 존재하면 Exception이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 내용이 맞는지 확인하기 위해 테스트 코드를 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스는 부모 역할을 할 PropagationService.java 와 자식 역할을 할 PropagationInternalService.java로 분리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서비스의 메소드에서는 데이터 저장하는 로직을 각각 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 트랜잭션의 오류에 대해 어떻게 처리되는지 확인해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PropagationService.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713678814762&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.infitry.laboratory.service.transaction.propagation;

import com.infitry.laboratory.entity.Member;
import com.infitry.laboratory.persistence.jpa.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class PropagationService {

    private final MemberRepository memberRepository;
    private final PropagationInternalService propagationInternalService;

    @Transactional
    public void requiredTransaction() {
        saveNewMember();
        try {
            propagationInternalService.required();
        } catch(RuntimeException e) {
            printLog(e);
        }
    }

    @Transactional
    public void requiresNewTransaction() {
        saveNewMember();
        try {
            propagationInternalService.requiresNew();
        } catch(RuntimeException e) {
            printLog(e);
        }
    }

    @Transactional
    public void supportsTransaction() {
        saveNewMember();
        try {
            propagationInternalService.supports();
        } catch(RuntimeException e) {
            printLog(e);
        }
    }

    @Transactional
    public void notSupportedTransactional() {
        saveNewMember();
        try {
            propagationInternalService.notSupported();
        } catch(RuntimeException e) {
            printLog(e);
        }
    }

    @Transactional
    public void nestedTransactional() {
        saveNewMember();
        try {
            propagationInternalService.nested();
        } catch(RuntimeException e) {
            printLog(e);
        }
    }

    @Transactional
    public void mandatoryTransactional() {
        saveNewMember();
        try {
            propagationInternalService.mandatory();
        } catch(RuntimeException e) {
            printLog(e);
        }
    }

    @Transactional
    public void neverTransactional() {
        saveNewMember();
        propagationInternalService.never();
    }

    private void saveNewMember() {
        Member member = new Member();
        member.setName(&quot;111&quot;);
        member.setNickName(&quot;32132&quot;);
        memberRepository.save(member);
    }

    private static void printLog(Exception e) {
        log.info(&quot;Exception 을 외부로 throw 하지 않는다.&quot;, e);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;text-align: start;&quot;&gt;PropagationInternalService.java&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713678841397&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.infitry.laboratory.service.transaction.propagation;

import com.infitry.laboratory.entity.Order;
import com.infitry.laboratory.persistence.jpa.OrderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static org.springframework.transaction.annotation.Propagation.*;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PropagationInternalService {

    private final OrderRepository orderRepository;

    @Transactional(propagation = REQUIRED)
    public void required() {
        log.info(&quot;활성된 트랜잭션이 있다면 기존 트랜잭션을 사용합니다.&quot;);
        saveNewOrder();
    }

    @Transactional(propagation = REQUIRES_NEW)
    public void requiresNew() {
        log.info(&quot;항상 새로운 트랜잭션을 사용합니다. 이미 진행중인 트랜잭션이 있다면 보류하고 해당 트랜잭션을 먼저 진행합니다.&quot;);
        saveNewOrder();
    }

    @Transactional(propagation = SUPPORTS)
    public void supports() {
        log.info(&quot;활성된 트랜잭션이 있다면 기존 트랜잭션을 사용하고 없으면 트랜잭션 없이 실행합니다.&quot;);
        saveNewOrder();
    }

    @Transactional(propagation = NOT_SUPPORTED)
    public void notSupported() {
        log.info(&quot;활성화 트랜잭션이 있던 없던 Transaction 없이 실행합니다.&quot;);
        saveNewOrder();
    }

    @Transactional(propagation = MANDATORY)
    public void mandatory() {
        log.info(&quot;부모 트랜잭션 내에서 실행되며, 부모 트랜잭션이 없을 경우 Exception 이 발생합니다.&quot;);
        saveNewOrder();
    }

    @Transactional(propagation = NESTED)
    public void nested() {
        log.info(&quot;부모 트랜잭션이 커밋될 때 같이 커밋, 자식 트랜잭션의 롤백은 부모 트랜잭션에 영향이 없습니다.&quot;);
        saveNewOrder();
    }

    @Transactional(propagation = NEVER)
    public void never() {
        log.info(&quot;트랜잭션 없이 실행되며 부모 트랜잭션이 존재하면 Exception 이 발생합니다.&quot;);
        saveNewOrder();
    }

    private void saveNewOrder() {
        Order order = new Order();
        order.setOrderName(&quot;신규주문-1&quot;);
        orderRepository.save(order);
        throw new RuntimeException(&quot;rollback&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PropagationServiceTest.java&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713678871924&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.infitry.laboratory.service.transaction.propagation;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class PropagationServiceTest {

    @Autowired
    PropagationService propagationService;


    @Test
    @DisplayName(&quot;required 테스트&quot;)
    public void required() {
        propagationService.requiredTransaction();
    }

    @Test
    @DisplayName(&quot;requires new 테스트&quot;)
    public void requiresNew() {
        propagationService.requiresNewTransaction();
    }

    @Test
    @DisplayName(&quot;supports 테스트&quot;)
    public void supports() {
        propagationService.supportsTransaction();
    }

    @Test
    @DisplayName(&quot;not Supported 테스트&quot;)
    public void notSupported() {
        propagationService.notSupportedTransactional();
    }

    @Test
    @DisplayName(&quot;nested 테스트&quot;)
    public void nested() {
        propagationService.nestedTransactional();
    }

    @Test
    @DisplayName(&quot;mandatory 테스트&quot;)
    public void mandatory() {
        propagationService.mandatoryTransactional();
    }

    @Test
    @DisplayName(&quot;never 테스트&quot;)
    public void never() {
        propagationService.neverTransactional();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository, Member, Order Entity 등은 간단하게 작성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 코드는 다음 리파지토리에서 확인하실 수 있습니다. (&lt;a title=&quot;Github&quot; href=&quot;https://github.com/infitry/laboratory/blob/master/src/test/java/com/infitry/laboratory/service/transaction/propagation/PropagationServiceTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. &lt;span style=&quot;text-align: start;&quot;&gt;REQUIRED&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 트랜잭션을 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SZ9KM/btsGM1Ecieb/fOUcIE4fVhYzTnXTKosB8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SZ9KM/btsGM1Ecieb/fOUcIE4fVhYzTnXTKosB8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SZ9KM/btsGM1Ecieb/fOUcIE4fVhYzTnXTKosB8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSZ9KM%2FbtsGM1Ecieb%2FfOUcIE4fVhYzTnXTKosB8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1230&quot; height=&quot;422&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 실행하면 다음과 같은 오류가 발생합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713679059177&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Transaction silently rolled back because it has been marked as rollback-only
org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상합니다. 분명 PropagationInternalService의 메서드는 Exception을 catch 하였는데 어째서 오류가 발생한 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REQUIRED는 부모 트랜잭션을 그대로 사용하게 됩니다. 동일 트랜잭션을 사용하다 보니 자식트랜잭션에서 발생한 예외에 대해 트랜잭션에 rollback-only 가 마크되었고 동일 트랜잭션을 사용하는 부모 트랜잭션에서 커밋을 할 때 이미 rollback-only 마크가 되어 롤백되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 자식 트랜잭션에서 예외를 throw 하지 않게 하면 어떻게 될까요?&lt;/p&gt;
&lt;pre id=&quot;code_1713679545725&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Transactional(propagation = REQUIRED)
    public void required() {
        try {
            log.info(&quot;활성된 트랜잭션이 있다면 기존 트랜잭션을 사용합니다.&quot;);
            saveNewOrder();
        } catch (Exception e) {
            log.error(&quot;error - &quot;, e);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 수정했습니다. 자식 트랜잭션에서 더 이상 예외를 외부로 던지지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1569&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMIaOM/btsGO7iLcNi/d5ItBw4BILx5P2sl7wBi50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMIaOM/btsGO7iLcNi/d5ItBw4BILx5P2sl7wBi50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMIaOM/btsGO7iLcNi/d5ItBw4BILx5P2sl7wBi50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMIaOM%2FbtsGO7iLcNi%2Fd5ItBw4BILx5P2sl7wBi50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1569&quot; height=&quot;274&quot; data-origin-width=&quot;1569&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그로 예외를 확인할 수 있고 테스트는 통과합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. &lt;span style=&quot;text-align: start;&quot;&gt;REQUIRES_NEW&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 새로운 트랜잭션을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 실행하면?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yRG1i/btsGPqWP34f/rjtT2KFOtdZskzwVXICRc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yRG1i/btsGPqWP34f/rjtT2KFOtdZskzwVXICRc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yRG1i/btsGPqWP34f/rjtT2KFOtdZskzwVXICRc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyRG1i%2FbtsGPqWP34f%2FrjtT2KFOtdZskzwVXICRc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;341&quot; height=&quot;102&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 트랜잭션을 생성해서 처리하기 때문에 앞서 발생한 UnexpectedRollbackException은 발생하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. &lt;span style=&quot;text-align: start;&quot;&gt;SUPPORTS&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;부모 트랜잭션이 있으면 사용하고 없으면 트랜잭션 없이 실행합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;155&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhj5zv/btsGPpcyGB2/SbAKQU3xtJw0oTWtF5CSbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhj5zv/btsGPpcyGB2/SbAKQU3xtJw0oTWtF5CSbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhj5zv/btsGPpcyGB2/SbAKQU3xtJw0oTWtF5CSbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhj5zv%2FbtsGPpcyGB2%2FSbAKQU3xtJw0oTWtF5CSbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1564&quot; height=&quot;155&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;155&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 트랜잭션을 사용하였고 rollback-only 마크가 되었기 때문에 마찬가지로 UnexpectedRollbackException이 발생합니다. 부모 트랜잭션이 없다면? 당연히 발생하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. &lt;span style=&quot;text-align: start;&quot;&gt;NOT_SUPPORTED&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 트랜잭션과 상관없이 트랜잭션 없이 실행합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;356&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O7VFa/btsGNttwse0/G4QcPNnBCfi0kGo4et9GXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O7VFa/btsGNttwse0/G4QcPNnBCfi0kGo4et9GXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O7VFa/btsGNttwse0/G4QcPNnBCfi0kGo4et9GXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO7VFa%2FbtsGNttwse0%2FG4QcPNnBCfi0kGo4et9GXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;356&quot; height=&quot;101&quot; data-origin-width=&quot;356&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히? 성공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5. NESTED&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 트랜잭션이 커밋될 때 같이 커밋, 자식 트랜잭션의 롤백은 부모 트랜잭션에 영향이 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4pWg1/btsGM2py6cd/Qu72IKxFpXK9m5MUVWwWn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4pWg1/btsGM2py6cd/Qu72IKxFpXK9m5MUVWwWn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4pWg1/btsGM2py6cd/Qu72IKxFpXK9m5MUVWwWn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4pWg1%2FbtsGM2py6cd%2FQu72IKxFpXK9m5MUVWwWn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1758&quot; height=&quot;108&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;108&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 시 다음과 같은 예외가 발생합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713681222738&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.springframework.transaction.NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider's capabilities&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서는 savepoints를 지원하지 않아 예외가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;6. &lt;span style=&quot;text-align: start;&quot;&gt;MANDATORY&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;부모 트랜잭션 내에서 실행되며, 부모 트랜잭션이 없을 경우 Exception이 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 트랜잭션이 존재할 때는 마찬가지로 UnexpectedRollbackException이 발생합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1601&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRfv67/btsGNsIa4aZ/TlkS6wtULk2gnPXGDSeqnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRfv67/btsGNsIa4aZ/TlkS6wtULk2gnPXGDSeqnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRfv67/btsGNsIa4aZ/TlkS6wtULk2gnPXGDSeqnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRfv67%2FbtsGNsIa4aZ%2FTlkS6wtULk2gnPXGDSeqnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1601&quot; height=&quot;110&quot; data-origin-width=&quot;1601&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 트랜잭션이 존재하지 않을 때는?&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713681027262&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 예외가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;7. NEVER&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 없이 실행되며 부모 트랜잭션이 존재하면 Exception이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 트랜잭션이 존재할 때 다음과 같은 예외가 발생합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713682129683&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Existing transaction found for transaction marked with propagation 'never'
org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 트랜잭션이 존재하지 않는다면? 기존 예외가 발생합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1567&quot; data-origin-height=&quot;171&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W8LYO/btsGMKW1nbN/qmEOm84MJKDlVMvtRg1Dak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W8LYO/btsGMKW1nbN/qmEOm84MJKDlVMvtRg1Dak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W8LYO/btsGMKW1nbN/qmEOm84MJKDlVMvtRg1Dak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW8LYO%2FbtsGMKW1nbN%2FqmEOm84MJKDlVMvtRg1Dak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1567&quot; height=&quot;171&quot; data-origin-width=&quot;1567&quot; data-origin-height=&quot;171&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional의 propagation 속성에 대해 알아보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 속성에 따른 자식 트랜잭션의 롤백이 부모트랜잭션에 미치는 영향도도 알아보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 여러 복잡한 로직들을 다룰 때 항상 트랜잭션을 확인하는 습관이 필요하다는 걸 다시 한번 더 느꼈습니다. &lt;/p&gt;</description>
      <category>백엔드/Spring</category>
      <category>@Transactional</category>
      <category>propagation</category>
      <category>rollback-only</category>
      <category>Spring</category>
      <category>Transactional</category>
      <author>infitry</author>
      <guid isPermaLink="true">https://infitry.tistory.com/81</guid>
      <comments>https://infitry.tistory.com/81#entry81comment</comments>
      <pubDate>Sun, 21 Apr 2024 16:01:26 +0900</pubDate>
    </item>
    <item>
      <title>클린코드 - 객체와 자료구조</title>
      <link>https://infitry.tistory.com/80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;본 글은 클린코드 6장을 읽고 난 후 개인적인 생각입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;자료 추상화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수를 비공개로 정의하는 이유는 변수에 의존하지 않게 만들기 위해서입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 수많은 프로그래머가 getter 혹은 setter 함수를 당연하게 공개해 비공개 변수를 외부에 노출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수를 private으로 선언하더라도 각 값마다 getter 함수와 setter 함수로 제공한다면 구현을 외부로 노출하는 셈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 자료를 세세하게 공개하기보다는 추상적인 개념의 함수를 만드는 것이 좋다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 setter에 대해서는 이 내용에 동의하지만 getter까지 전부 추상적인 개념(클래스 내부의 어떤 비공개 인스턴스 변수를 가져오는지 모르도록 추상화)으로 일일이 변경해야 한다는 부분에서는 클래스 용도마다 차이가 있을 것으로 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;자료/객체 비대칭&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;절차적인 코드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 자료 구조를 추가하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;객체 지향 코드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상화 인터페이스에 새로운 함수를 추가하기 어렵다. (모든 클래스를 고쳐야 하기 때문에)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 코드가 있다고 하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;절차적인 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713072392729&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Square {
	public Point topLeft;
    public double side;
}

public class Circle {
	public Point center;
    public double radius;
}

public class Geometry {
	public final double PI = 3.141592653589793;
    
    public double area(Object shape) {
    	if (shape instanceof Square) {
        	Square square = (Square) shape;
            return square.side * square.side;
        } else if (shape instanceOf Circle) {
        	Circle c = (Circle) shape;
            return PI * c.radius * c.radius;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;객체지향 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713072547257&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Shape {
	double area();
}

public class Square implements Shape {
	public Point topLeft;
    public double side;
    
    @Override
    public double area() {
    	return side * side;
    }
}

public class Circle implements Shape {
	public Point center;
    public double radius;
    public final double PI = 3.141592653589793;
    
    @Override
    public double area() {
    	return PI * radius * radius;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절차적인 코드에서는 새 함수를 추가하여도 다른 클래스들이 영향받지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 지향 코드에서는 Shape에 함수를 추가한다면 구현체인 Circle, Square 모두가 영향을 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개인적인 생각으로는 추상화한 Shape 에 함수가 추가되어야 하는 경우는 구현체들에 모두 추가되어야 할 때인 것 같은데 딱히 단점으로 느껴지진 않고 영향을 받는 게 당연한 것 같다고 생각이 듭니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특정 하나의 구현체만 적용되어야 한다면 다른 방법을 사용하는 게 좋아 보이네요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향 코드는 새 클래스를 추가하기 쉽습니다. 위 코드에서 Shape를 구현하는 클래스만 추가하여 함수를 구현하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;디미터 법칙&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다.&quot;라고 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 클래스 C&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- f 가 생성한 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- f 인수로 넘어온 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- C 인스턴스 변수에 저장된 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 코드가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713073049972&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 위와 같은 코드를 기차 충돌이라고 부른다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 객차가 한 줄로 이어진 기차처럼 보이기 때문인데 이러한 코드는 다음과 같이 바꾸는 게 좋은 것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713073205032&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;자료 전달 객체&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;활성 레코드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;활성 레코드는 데이터베이스 테이블이나 다른 소스에서 자료를 직접 반환한 결과입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;활성 레코드에 비즈니스 규칙 메서드를 추가해 이런 자료 구조를 객체로 취급하는 개발자가 흔하다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적인 생각으로는 상황에 따라 다를 것 같기는 합니다. 프로젝트 초반 단계에서 빠르게 속도를 내기 위해서는 3 계층 아키텍처 + JPA Entity(책에서 말하는 활성레코드로 추정)에 비즈니스 로직을 추가할 수도 있을 것 같습니다. 다만 점점 도메인 로직이 거대해진다면 활성 레코드 객체와 도메인 로직을 구현하는 객체를 별개로 두는 게 더 좋을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체는 동작을 공개하고 자료를 숨긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절차적인 코드, 객체 지향 코드는 상황에 따라 활용될 수 있다.&lt;/p&gt;</description>
      <category>클린코드와 리팩토링/클린코드</category>
      <category>clean code</category>
      <category>객체와 자료구조</category>
      <author>infitry</author>
      <guid isPermaLink="true">https://infitry.tistory.com/80</guid>
      <comments>https://infitry.tistory.com/80#entry80comment</comments>
      <pubDate>Sun, 14 Apr 2024 14:51:00 +0900</pubDate>
    </item>
    <item>
      <title>스프링부트 3.x 에서 트레이싱 기능 추가하기</title>
      <link>https://infitry.tistory.com/79</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링부트 3.x부터 Spring cloud sleuth로 트레이싱 기능을 추가할 수 없게 되었습니다.&lt;br /&gt;마이그레이션 가이드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1712465691133&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Spring Cloud Sleuth 3.1 Migration Guide&quot; data-og-description=&quot;Provides tracing abstractions over tracers and tracing system reporters. - micrometer-metrics/tracing&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide&quot; data-og-url=&quot;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bhUKtH/hyVJZELtjb/Wss0kMDSoEgbK5G7fOcjSk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bhUKtH/hyVJZELtjb/Wss0kMDSoEgbK5G7fOcjSk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Cloud Sleuth 3.1 Migration Guide&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Provides tracing abstractions over tracers and tracing system reporters. - micrometer-metrics/tracing&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 Micrometer 의 brave  의존성을 추가해 트레이싱 기능을 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용해 볼 환경은 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring boot 3.1.3&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;java 17&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 micrometer 의 brave 의존성을 추가해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zipkin 은 사용하지 않아 제외해 주었습니다. 사용하시는 분들은 exclude를 제거해 주세요.&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;implementation(&quot;io.micrometer:micrometer-tracing-bridge-brave&quot;) {
    exclude group: &quot;io.zipkin.reporter2&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;traceId 및 spanId를 확인할 수 있도록 로그 패턴을 변경해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 logback xml 파일에서 설정하였습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[%-5level] %d{yyyy-MM-dd HH:mm:ss} [%thread] %replace([%X{traceId}, %X{spanId}]){'\[, \]',''}[%logger{0}:%line] - %msg%n&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;%replace 부분을 쓰지 않으면 traceId, spanId 가 없을 때 빈 값이 보이게 됩니다. &lt;br /&gt;ex) 2024-04-07T14:14:20.632+09:00 [INFO ] 2024-04-07 14:14:20 [restartedMain]&lt;b&gt; [, ]&lt;/b&gt; [OptionalLiveReloadServer:59] - LiveReload server is running on port 35729&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 가지 상황을 테스트해보기 위해 아래와 같은 컨트롤러 서비스를 만듭니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AsyncConfig.java&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@EnableAsync
@Configuration
public class AsyncConfig {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EnableAsync를 통해 @Async 기능을 활성화해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TracingService.java&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class TracingService {

    public void print() {
        log.info(&quot;기존 스레드를 입니다.&quot;);
    }
    @Async
    public void asyncPrint() {
        log.info(&quot;@Async 에 의한 스레드 입니다.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지 케이스에 대한 테스트를 진행하기 위해 메서드를 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 기본 호출 print()&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. @Async 어노테이션을 통한 비동기 호출 asyncPrint()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Executors를 통한 비동기 호출 executorsPrint()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TracingController.java&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;import com.infitry.laboratory.service.tracing.TracingService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/tracing&quot;)
public class TracingController {

	private final Executor executor;
    private final TracingService tracingService;

    @GetMapping(&quot;/test&quot;)
    public void testTracing() {
        tracingService.print();
        executor.execute(() -&amp;gt; log.info(&quot;executors 에 의한 스레드입니다.&quot;));
        tracingService.asyncPrint();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 메서드를 호출하도록 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 테스트로 컨트롤러를 호출합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1712468243043&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[INFO ] 2024-04-07 14:36:26 [http-nio-8080-exec-1] [661230da8761fca64322fbb40c3b2848, 4322fbb40c3b2848][TracingService:14] - 기존 스레드를 입니다.
[INFO ] 2024-04-07 14:36:26 [pool-5-thread-1] [TracingService:23] - executors 에 의한 스레드입니다.
[INFO ] 2024-04-07 14:36:26 [task-1] [TracingService:18] - @Async 에 의한 스레드 입니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상했던 결과는 3가지 케이스 모두 traceId, spanId 가 생기길 기대하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 기본 호출에 대해서만 생성되었습니다. [661230da8761fca64322fbb40c3b2848, 4322fbb40c3b2848]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서를 확인해 보니 비동기 스레드에 대해 tracing 기능을 사용하려면 별도로 커스터마이징이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;참고&quot; href=&quot;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide#async-instrumentation&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide#async-instrumentation&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1712468410428&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Spring Cloud Sleuth 3.1 Migration Guide&quot; data-og-description=&quot;Provides tracing abstractions over tracers and tracing system reporters. - micrometer-metrics/tracing&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide#async-instrumentation&quot; data-og-url=&quot;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cQrU6j/hyVJ4Tz38f/3CjFjKK2ejCACWdeI0wygk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide#async-instrumentation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide#async-instrumentation&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cQrU6j/hyVJ4Tz38f/3CjFjKK2ejCACWdeI0wygk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Cloud Sleuth 3.1 Migration Guide&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Provides tracing abstractions over tracers and tracing system reporters. - micrometer-metrics/tracing&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일하게 적용하기 위해 기존 AsyncConfig 다음과 같이 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;* 위 예시코드에서 ContextSnapshotFactory 인터페이스는 스프링부트 3.1.3 이상부터 존재하는 것 같네요.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1712469852288&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import io.micrometer.context.ContextExecutorService;
import io.micrometer.context.ContextScheduledExecutorService;
import io.micrometer.context.ContextSnapshot;
import io.micrometer.context.ContextSnapshotFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.concurrent.*;

@EnableAsync
@Configuration(proxyBeanMethods = false)
public class AsyncConfig implements AsyncConfigurer, WebMvcConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        return ContextExecutorService.wrap(Executors.newCachedThreadPool(), ContextSnapshot::captureAll);
    }

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        configurer.setTaskExecutor(new SimpleAsyncTaskExecutor(r -&amp;gt; new Thread(ContextSnapshotFactory.builder().build().captureAll().wrap(r))));
    }

    @Bean
    public ThreadPoolTaskScheduler taskExecutor() {
        var threadPoolTaskScheduler = new ThreadPoolTaskScheduler() {
            @Override
            public ScheduledExecutorService getScheduledExecutor() throws IllegalStateException {
                return ContextScheduledExecutorService.wrap(super.getScheduledExecutor());
            }
        };
        threadPoolTaskScheduler.initialize();
        return threadPoolTaskScheduler;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후 다시 실행해 봅니다.&lt;/p&gt;
&lt;pre id=&quot;code_1712469938667&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[INFO ] 2024-04-07 15:02:35 [http-nio-8080-exec-1] [661236fbbd55bf9f47bf8c35a9b5fb44, 47bf8c35a9b5fb44][TracingService:14] - 기존 스레드를 입니다.
[INFO ] 2024-04-07 15:02:35 [taskExecutor-1] [661236fbbd55bf9f47bf8c35a9b5fb44, 47bf8c35a9b5fb44][TracingController:24] - executors 에 의한 스레드입니다.
[INFO ] 2024-04-07 15:02:35 [pool-5-thread-1] [661236fbbd55bf9f47bf8c35a9b5fb44, 47bf8c35a9b5fb44][TracingService:18] - @Async 에 의한 스레드 입니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 스레드명은 모두 다르나 (http-nio-8080-exec-1, taskExecutor-1, poo-5-thread-1)&lt;br /&gt;traceId, spanId(661236fbbd55bf9f47bf8c35a9b5fb44, 47bf8c35a9b5fb44) 는 모두 같아 비동기 호출이 존재하더라도 해당 요청을 추적 할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 sleuth 에서 micrometer 로 프로젝트가 이동한지 얼마되지 않아 불편한 점이 많은 것 같습니다. (async wrap 등..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확한 부분은 조금 더 여러가지 상황에서 테스트 해봐야 알 수 있을 것 같습니다.&lt;/p&gt;</description>
      <category>백엔드/Spring Boot</category>
      <category>micrometer</category>
      <category>spring boot 로그</category>
      <category>spring cloud sleuth</category>
      <category>Trace</category>
      <category>tracing</category>
      <category>로그</category>
      <category>로그 추적</category>
      <author>infitry</author>
      <guid isPermaLink="true">https://infitry.tistory.com/79</guid>
      <comments>https://infitry.tistory.com/79#entry79comment</comments>
      <pubDate>Sun, 7 Apr 2024 15:10:38 +0900</pubDate>
    </item>
    <item>
      <title>클린코드 - 형식 맞추기</title>
      <link>https://infitry.tistory.com/78</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;본 글은 클린코드 5장을 읽고 난 후 주관적인 생각입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;은 탄환은 없다&quot;라는 말처럼 항상 상황에 맞춰 생각을 한 번 더 하는 습관이 중요한 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;형식을 맞추는 목적&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜 시간이 지나도 맨 처음 잡아놓은 구현 스타일과 가독성 수준은 유지보수 용이성과 확장성에 계속해서 영향을 미칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 너무 엄격하지 않은 어느정도 스타일과 규율은 필요하다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;적절한 행 길이를 유지하라&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 500줄을 넘지않고 200줄 정도인 파일로도 커다란 시스템을 구축할 수 있다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적인 생각으로는 파일의 길이가 길어지는 건 하나의 클래스가 너무 많은 일을 하고 있을 때 그렇다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주문이라는 큰 상위개념이 있고 하위개념으로는 옵션, 가격, 주문상품 등이 있다고 했을 때&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문이라는 큰 개념으로 클래스를 만들어 해당 클래스에서 하위개념에서 할 수 있는 모든 책임을 처리하려 할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일이 길어지는 경우를 종종 본 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행 길이가 몇 만자가 넘어가면 IDE에서도 버벅거리는 현상도 발생하고, 가독성, 유지보수성이 너무 떨어지는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;개념은 빈 행으로 분리하라&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 행은 수식이나 절을 나타내고, 일련의 행 묶음은 완결된 생각 하나를 표현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 완결된 생각 하나를 구분지으면 좀 더 읽기 편해지는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 다음과 같은 코드는 읽기 어렵지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711761194315&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BoldWidget {
	public static final String A = &quot;A&quot;;
    public BoldWidget(ParentWidget parent, String text) throws Exception {
    	super(parent);
        Matcher match = pattern.matcher(text);
        match.find();
     	...
    }
    
    public render() throws Exception {
    	StringBuffer html = new StringBuffer(&quot;&amp;lt;b&amp;gt;&quot;);
        html.append(childHtml()).append(&quot;&amp;lt;/b&amp;gt;&quot;);
        return html.toString();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 빈 행을 사용하지 않는다면 다음과 같이 읽기 어려워집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711761279955&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BoldWidget {
	public static final String A = &quot;A&quot;;
    public BoldWidget(ParentWidget parent, String text) throws Exception {
    	super(parent);
        Matcher match = pattern.matcher(text);
        match.find(); ...}
    public render() throws Exception {
    	StringBuffer html = new StringBuffer(&quot;&amp;lt;b&amp;gt;&quot;);
        html.append(childHtml()).append(&quot;&amp;lt;/b&amp;gt;&quot;);
        return html.toString();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;세로 밀집도&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 밀집도는 연관성을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 밀집한 코드 행은 세로로 가까이 놓여야 한다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;수직 거리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- 변수 선언&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수는 최대한 가까이 선언합니다. 사용하기 전에 선언하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 인스턴스 변수는 클래스 맨 처음에 선언하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- 종속함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- 개념적 유사성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 말하면 비슷한(친화도가 높은) 함수끼리 모아놓거나, 종속되는 함수는 서로 가까이 배치해 놓는 게 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;가로형식 맞추기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 행의 가로는 각 팀원의 모니터 크기나 글꼴마다 다르겠지만 모든 팀원이 스크롤이 생기지 않도록 적절하게 선택하는 게 좋은 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;가로정렬&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로는 정말 필요하지 않다고 생각합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711761854712&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Align() {
	private String name;
    private String alignStyle;
    private Object object;
    
    public Align(String name, Style style, Object target) {
        this.name =			name;
        this.alginStyle =		style;
        this.object =			target;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 엉뚱한 부분을 강조해 진짜 의도가 가려집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;들여 쓰기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 중요하다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;들여 쓰기의 유무에 따라 가독성은 천차만별이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 간단한 if, for 문 같은 경우라도 들여 쓰기를 하는 것이 좋다고 생각합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;팀 규칙&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머라면 각자 선호하는 규칙이 있으나 팀에 속한다면 팀 규칙을 따라야 한다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이뤄진다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 스타일의 소스 파일들로 이뤄진다면 좀 더 읽기 좋은 코드가 될 것 같습니다.&lt;/p&gt;</description>
      <category>클린코드와 리팩토링/클린코드</category>
      <category>clean code</category>
      <category>클린코드</category>
      <author>infitry</author>
      <guid isPermaLink="true">https://infitry.tistory.com/78</guid>
      <comments>https://infitry.tistory.com/78#entry78comment</comments>
      <pubDate>Sat, 30 Mar 2024 10:31:31 +0900</pubDate>
    </item>
    <item>
      <title>GIT 리파지토리 마이그레이션 하기</title>
      <link>https://infitry.tistory.com/77</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;어찌하다 보니 Gitlab repository에서 약 30여 개 프로젝트를 Github 리파지토리로 마이그레이션 하는 경우가 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단은 기존 저장소를 복제합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711718254174&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git clone --bare {복제할 저장소주소}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그 후 복제한 저장소의 폴더로 접근해&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1711718269353&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd {복제할 저장소주소}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 옮길 저장소로 이동시킵니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711718277443&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git push --mirror {옮길 저장소주소}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 여러 개의 리파지토리를 옮기는 쉘 스크립트를 작성한 후 실행 시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;* 한 번 clone 하여 push 하면 그 이후에 재실행하면 변경된 부분만 push 할 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 옮기는 와중에 특정 몇몇 리파지토리가 push 할 때 다음과 같은 오류메시지와 함께 push 가 되지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[remote rejected] ***** (failure)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류메시지는 명확하지 않았고, 구글링을 해봐도 구글상태에 관한 이야기만 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 해결한 방법은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;문제 1.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옮겨야할 브랜치가 적은 리파지토리는 오류가 발생하지 않고 브랜치가 많은 리파지토리에서만 오류가 발생하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 main 브랜치에 병합되어 더 이상 사용하지 않는 아주 오래된 브랜치들을 정리하기 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 다시 push를 하니 정상적으로 되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;문제 2.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 특정 리파지토리는 사용하지 않는 브랜치 수가 적어졌는데도 불구하고 push 되지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남아있던 브랜치 중 사용하지 않는 보호된 브랜치가 있는 것을 발견하고 이미 사용하지 않는 걸 확인하고 이상하다 싶어 제거했더니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 push 가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 다음에 고생하실 다른 분들을 위해 글로 남깁니다.&lt;/p&gt;</description>
      <category>인프라/GIT</category>
      <category>git</category>
      <category>git clone</category>
      <category>git push</category>
      <category>git repository</category>
      <category>github</category>
      <category>gitlab</category>
      <category>remote rejected</category>
      <author>infitry</author>
      <guid isPermaLink="true">https://infitry.tistory.com/77</guid>
      <comments>https://infitry.tistory.com/77#entry77comment</comments>
      <pubDate>Fri, 29 Mar 2024 22:19:12 +0900</pubDate>
    </item>
    <item>
      <title>Spring-data-jpa 사용 시 @Transactional 은 꼭 있어야 할까?</title>
      <link>https://infitry.tistory.com/76</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;코드리뷰를 하던 중 동료 개발자 분 코드에서 1건 저장 행위만 하는 Service 레이어 메서드의 @Transactional 이 없는 것을 발견했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나: &quot;어? 이게 있어야하지 않을까요?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동료 개발자 : &quot;1개 만 저장하는 건데 있어야 하나요?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해 보니 딱히 이유를 찾지 못했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 spring-data-jpa 구현체인 &lt;b&gt;SimpleJpaRepository의&lt;/b&gt; save 메서드에서 @Transactional 이 걸려있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BsCyY/btsF025hQjQ/u4zxdVkkxNtQjUkXfz1yL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BsCyY/btsF025hQjQ/u4zxdVkkxNtQjUkXfz1yL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BsCyY/btsF025hQjQ/u4zxdVkkxNtQjUkXfz1yL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBsCyY%2FbtsF025hQjQ%2Fu4zxdVkkxNtQjUkXfz1yL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;522&quot; height=&quot;300&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 제가 본 강의에서는 서비스 레이어의 클래스에는 @Transactional(readOnly=true) 어노테이션을 붙이고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 메서드에는 @Transactional을 붙이는 방식을 많이 활용한다고 했는데 이유가 기억나지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 검색해 본 결과 다음과 같은 이유를 찾게 되었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. JPA 영속성 컨텍스트에 의해 동작하는 변경감지, 지연로딩이 동작하지 않는다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;지연로딩&lt;/b&gt;&lt;br /&gt;@Transactional 이 걸려있지 않다면 변경감지 지연로딩이 동작하지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 간단한? 엔티티가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;511&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzavnY/btsF016zUbc/nVrOaEtId5xZ1XDySXwrK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzavnY/btsF016zUbc/nVrOaEtId5xZ1XDySXwrK0/img.png&quot; data-alt=&quot;Member Entity&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzavnY/btsF016zUbc/nVrOaEtId5xZ1XDySXwrK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzavnY%2FbtsF016zUbc%2FnVrOaEtId5xZ1XDySXwrK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;511&quot; height=&quot;318&quot; data-origin-width=&quot;511&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Member Entity&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Member 엔티티 에는 관계를 맺고 있는 MemberGroup이라는 엔티티가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FetchType.LAZY로 지연로딩하게 설정되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;127&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WpeTz/btsF16shrxr/4VGJyjIXR5n3EKk7y8Jbp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WpeTz/btsF16shrxr/4VGJyjIXR5n3EKk7y8Jbp0/img.png&quot; data-alt=&quot;findMemberLazyWithoutTransactional&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WpeTz/btsF16shrxr/4VGJyjIXR5n3EKk7y8Jbp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWpeTz%2FbtsF16shrxr%2F4VGJyjIXR5n3EKk7y8Jbp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;737&quot; height=&quot;127&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;127&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;findMemberLazyWithoutTransactional&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 메서드가 있다고 했을 때 Member 엔티티를 조회한 후 MemberGroup 엔티티를 순회하며 code를 출력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(물론 출력하지 않는다고 해도 보통의 경우는 엔티티를 DTO로 변환하거나 하여 지연로딩이 실행됩니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1373&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbANqW/btsF3lhVVA4/CGHNQ7yHOIC3H2efX2XizK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbANqW/btsF3lhVVA4/CGHNQ7yHOIC3H2efX2XizK/img.png&quot; data-alt=&quot;지연로딩 테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbANqW/btsF3lhVVA4/CGHNQ7yHOIC3H2efX2XizK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbANqW%2FbtsF3lhVVA4%2FCGHNQ7yHOIC3H2efX2XizK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1373&quot; height=&quot;253&quot; data-origin-width=&quot;1373&quot; data-origin-height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;지연로딩 테스트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 실행을 시켜보면&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zj4H5/btsF1RvkduJ/DsW7S0uEyp062KWcWEzgQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zj4H5/btsF1RvkduJ/DsW7S0uEyp062KWcWEzgQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zj4H5/btsF1RvkduJ/DsW7S0uEyp062KWcWEzgQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzj4H5%2FbtsF1RvkduJ%2FDsW7S0uEyp062KWcWEzgQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;317&quot; height=&quot;90&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LazyInitializationException 이 발생하게 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 당장 엔티티에 관계를 맺고 있지 않더라도 추후 다른 사람이 관계를 추가하게 될 수도 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 미리 붙여놓는 게 좋을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;변경감지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;/b&gt;방금 본 Member 엔티티의 값을 변경해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;member의 이름은 초기에 &quot;tester1&quot;로 저장되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;578&quot; data-origin-height=&quot;119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs5lW9/btsF1r4OvzF/RJ5EmSIkOCQ3KobmCkfv60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs5lW9/btsF1r4OvzF/RJ5EmSIkOCQ3KobmCkfv60/img.png&quot; data-alt=&quot;변경감지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs5lW9/btsF1r4OvzF/RJ5EmSIkOCQ3KobmCkfv60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs5lW9%2FbtsF1r4OvzF%2FRJ5EmSIkOCQ3KobmCkfv60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;578&quot; height=&quot;119&quot; data-origin-width=&quot;578&quot; data-origin-height=&quot;119&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;변경감지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 영속성 컨텍스트의 변경감지에 의해 값이 변경되길 기대하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7nYHy/btsF1Z7MwrQ/P6WUuXRIiksqVFLD3swEq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7nYHy/btsF1Z7MwrQ/P6WUuXRIiksqVFLD3swEq0/img.png&quot; data-alt=&quot;변경감지 테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7nYHy/btsF1Z7MwrQ/P6WUuXRIiksqVFLD3swEq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7nYHy%2FbtsF1Z7MwrQ%2FP6WUuXRIiksqVFLD3swEq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;914&quot; height=&quot;270&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;변경감지 테스트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 테스트를 실행하였을 때 변경감지에 의해 값이 변경되었다면 두 멤버의 이름은 달라져야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;77&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qcZ19/btsF1AtQTOC/MJYktjZj6LVDWGeKdfZgok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qcZ19/btsF1AtQTOC/MJYktjZj6LVDWGeKdfZgok/img.png&quot; data-alt=&quot;변경감지 테스트 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qcZ19/btsF1AtQTOC/MJYktjZj6LVDWGeKdfZgok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqcZ19%2FbtsF1AtQTOC%2FMJYktjZj6LVDWGeKdfZgok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;316&quot; height=&quot;77&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;77&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;변경감지 테스트 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 변경되지 않아 테스트가 통과하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 &lt;b&gt;&quot;꼭 JPA의 변경감지 기능을 사용해야 할까?&quot;&lt;/b&gt; 라고 생각을 할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경감지 기능을 쓰면 병합을 쓸 때보다 안전합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접적으로 SimpleJpaRepository의 save 메서드를 통해 병합을 하게 되면 모든 필드를 변경합니다. &lt;br /&gt;예를 들어 특정 엔티티가 생성 시는 10개의 필드를 입력하지만 수정 시 3개의 필드만 수정가능하다고 했을 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준영속 상태의 엔티티를 병합하게 된다면 모든 필드를 입력하여 병합하게 됩니다.&lt;br /&gt;(null 혹은 빈 값으로 업데이트할 위험이 있습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 의식적으로 변경감지를 쓰게 된다면 @Id로 영속 상태의 엔티티를 가져오고 필요한 부분만 수정하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. @Transactional(readOnly=true)를 &lt;span&gt;붙여주면&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;성능이&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;향상된다&lt;/span&gt;.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 마다 다르지만 읽기 전용 모드로 동작할 수 있습니다.&lt;/li&gt;
&lt;li&gt;변경감지 기능을 사용하지 않습니다.&lt;/li&gt;
&lt;li&gt;영속성&amp;nbsp;컨텍스트&amp;nbsp;flush를&amp;nbsp;하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 개인적인 생각일 수도 있지만 추후 작업 시 다른 사람의 부담을 줄여줍니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 데이터만 저장하고 있던 서비스 레이어의 메서드에서 다른 작업이 추가가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 @Transactional 어노테이션이 걸려있다면, 다른 사람은 트랜잭션에 대한 고민을 하지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 이유로 스프링 + JPA를 사용 중이라면 서비스 레이어 클래스에는 @Transactional(readOnly=true) 어노테이션을 붙여 모든 메서드를 기본적으로 read only 트랜잭션으로 동작하게 만들고, 쓰기 작업을 하는 메서드의 경우만 @Transactional 어노테이션을 붙여주는 게 아무런 고민을 해도 되지 않아 좋은 것 같습니다. &lt;/p&gt;</description>
      <category>백엔드/JPA</category>
      <category>@Transactional</category>
      <category>JPA</category>
      <category>spring boot</category>
      <category>변경감지</category>
      <category>지연로딩</category>
      <author>infitry</author>
      <guid isPermaLink="true">https://infitry.tistory.com/76</guid>
      <comments>https://infitry.tistory.com/76#entry76comment</comments>
      <pubDate>Sun, 24 Mar 2024 18:27:53 +0900</pubDate>
    </item>
    <item>
      <title>Flutter 구글 로그인 개발 시 안드로이드 idToken null</title>
      <link>https://infitry.tistory.com/75</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;flutter로 firebase , google_sign_in를 이용하여 구글로그인 구현 시 IOS에서는 문제가 없으나 &lt;br /&gt;AOS에서만 idToken 이 null로 응답되는 문제가 발생하였습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;@override
Future&amp;lt;String&amp;gt; authorize() async {
  final googleUser = await GoogleSignIn().signIn();
  final googleAuth = await googleUser?.authentication;
  return googleAuth?.idToken ?? &quot;&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flutter google_sign_in 라이브러리를 통해 개발하게 되면 authentication을 통해 idToken을 얻어올 수 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 값으로 서버에서 유효한 토큰인지 체크합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 안드로이드에서만 null로 나오는 문제가 발생하였고 계속하여 검색하기 시작했습니다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;firebase 가이드에는 안드로이드 프로젝트 레벨의 build.gradle과 모듈 레벨의 build.gradle 에 각각 의존성을 추가하라고 되어있지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 의존성들을 추가하게 되면 코틀린 버전 충돌부터.. 수많은 문제들을 만나게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 strings.xml 에 default_web_client_id를 추가하여 해결할 수 있었습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;&amp;lt;resources&amp;gt;
    &amp;lt;!-- Google Login (Web Client ID) --&amp;gt;
    &amp;lt;string name=&quot;default_web_client_id&quot;&amp;gt;{client_id}&amp;lt;/string&amp;gt;
    &amp;lt;!-- // Google Login --&amp;gt;
&amp;lt;/resources&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;firebase에서 안드로이드 앱을 만들면 다운로드할 수 있는 google-services.json 파일을 확인하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. oauth_client의 client_type 이 &quot;3&quot;인 client_id를 default_web_client_id의 {client_id} 부분에 넣습니다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;혹시나 client_type 3인 client_id 가 없다면 firebase에서 그냥 웹앱 애플리케이션 하나 더 추가한 뒤 google-services.json 파일을 다시 받습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 해당 client_id를 서버에서 인증할 때도 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 방법으로 해결은 되었으나 왠지 strings.xml 에 네이버로그인할 때 사용되었던&lt;br /&gt;&amp;lt;string name=&quot;client_id&quot;&amp;gt; {naver_client_id}&amp;lt;/string&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분과 충돌이 있지 않았나 쉽기도 하네요.. &lt;b&gt;(제거하고 해보진 않았습니다.)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저보다는 더 쉽게 찾아서 해결하시길 바라며 기록합니다...&lt;/p&gt;</description>
      <category>프론트엔드/Flutter</category>
      <category>flutter</category>
      <category>google_sign_in</category>
      <category>google_sign_in android idToken is null</category>
      <category>google_sign_in idToken</category>
      <author>infitry</author>
      <guid isPermaLink="true">https://infitry.tistory.com/75</guid>
      <comments>https://infitry.tistory.com/75#entry75comment</comments>
      <pubDate>Sun, 7 Jan 2024 21:36:05 +0900</pubDate>
    </item>
    <item>
      <title>SOAP connection reset</title>
      <link>https://infitry.tistory.com/74</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;글을 읽기 전에 앞 서 모든 connection reset 오류 메시지 해결책에 대해 해당하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 가지 오류가 있는 것으로 알고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; stack trace를 잘 살펴보면 더 세밀한 정보를 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제발생&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOAP 관련한 통신을 JAXWS 라이브러리를 통해 사용하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOAP 관련 통신 플랫폼 자체를 변경하게 되어 스프링부트 3.x로 버전을 올리고 관련 라이브러리 들도 전부 버전을 올리게 되었는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간헐적으로 &lt;b&gt;Could not send message.&lt;/b&gt; 라는 오류 메시지와 함께 SOAP 요청이 목적지 API에 전달되지 않는 오류가 발생하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 내용을 자세히 파악해보니? &lt;b&gt;connection reset&lt;/b&gt; 이라는 오류 메시지도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 파보니 &lt;b&gt;ConnectionExpiredException&lt;/b&gt; 이 발생하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 플랫폼과 비교해 보니 다른 점이 기존에는 SOAP 통신 시 사용하는 Client class 가 라이브러리 자체에서 ClientProxy를 통해 (예상)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정된 java에 포함된 HTTPUrlConnection을 사용하여 통신하게 되어 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 버전은 cxf 를 통해 생성된 클래스 파일 중 SOAP 통신을 할 수 있는 클래스파일이 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 클래스 파일에서 HTTPClient에 대한 여러 옵션을 설정하려니? ClientProxy.getClient() 메서드를 통해 해당 서비스에서 사용되는 클라이언트 객체를 가져올 순 있지만 헤더 설정 등 변경할 수 있는 옵션에 한계가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;가설&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트 3.x 버전으로 올리면서 spring-boot-starter-web-services 의 버전도 함께 올라가면서 변경된 건지 jaxws 라이브러리 버전이 변경되면서 사용하는 클라이언트가 변경된 건지는 정확히 알 수 없으나 일단은 통신하는 HTTPClient 가 문제라는 사실은 알아냈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅해보니 기존 버전에서는 HTTPUrlConnection 클래스를 사용하여 타 API와 통신하였고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 버전의 플랫폼에서는 HTTPClientImp과 SocketTube 클래스를 사용하여 HTTP 요청을 한다는 사실을 알아냈고 무엇인가 요청을 시도하는 클라이언트 옵션 때문이라는 가설이 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 생각한 가설은 HTTP 1.1 의 Keep-alive로 인해 이미 연결이 만료된 커넥션을 클라이언트 측에서 재사용 시도하다가 오류가 발생한 것으로 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-ws/docs/current/reference/html/#client-web-service-template&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-ws/docs/current/reference/html/#client-web-service-template&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트 Docs 에 예시로 있는 소스코드를 발견하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통신 부분의 소스코드를 수정하기 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존 : .wsdl 파일 기반으로 apache-cxf 플러그인으로 생성된 클래스파일을 통해 통신&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;변경: WebServiceTemplate 클래스를 통한 통신&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 소스코드에서는 MessageSender를 HttpComponentsMessageSender 로 변경하고 apache HttpClient를 사용하여 클라이언트 측 커넥션 풀과 커넥션에 대해 여러 가지 옵션을 조정할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOAP-Client 예제 코드를 적용한 GIT Repository 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/infitry/soap-client/tree/master&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/infitry/soap-client/tree/master&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드에서 유휴 상태의 커넥션 제거주기 &lt;b&gt;(evictIdleConnections(40, TimeUnit.SECONDS)와&lt;/b&gt; &lt;br /&gt;만료된 커넥션 &lt;b&gt;(evictExpiredConnections)을&lt;/b&gt; 제거하는 옵션을 추가하고 정상 동작하는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 해당 옵션을 주고 로그 레벨을 DEBUG로 설정하게 되면 다음과 같은 메시지와 함께 열심히 정리해 줍니다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PoolingHttpClientConnectionManager : Closing expired connections&lt;/b&gt;&lt;br /&gt;&lt;b&gt;PoolingHttpClientConnectionManager : Closing connections idle longer than 40000 MILLISECONDS&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확한 건 아니지만 &lt;b&gt;ConnectionExpiredException &lt;/b&gt;이 발생한 것만 보면 이미 만료된 커넥션을 재사용하여 문제가 되었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 apache HttpClient 설정 예시입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1690092567227&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package infitry.soap.client.configuration;

import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.transport.http.HttpComponentsMessageSender;

import java.util.concurrent.TimeUnit;

@Configuration
public class CountryConfiguration {
    @Bean
    public Jaxb2Marshaller marshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        // this package must match the package in the &amp;lt;generatePackage&amp;gt; specified in
        // pom.xml
        marshaller.setContextPath(&quot;infitry.soap.client.wsdl&quot;);
        return marshaller;
    }

    @Bean
    public WebServiceTemplate countryWebServiceTemplate(Jaxb2Marshaller marshaller) {
        var webServiceTemplate = new WebServiceTemplate();
        webServiceTemplate.setMessageSender(createMessageSender());
        webServiceTemplate.setMarshaller(marshaller);
        webServiceTemplate.setUnmarshaller(marshaller);
        return webServiceTemplate;
    }

    private HttpComponentsMessageSender createMessageSender() {
        var httpComponentsMessageSender = new HttpComponentsMessageSender();
        httpComponentsMessageSender.setConnectionTimeout(10000);
        httpComponentsMessageSender.setReadTimeout(60000);
        httpComponentsMessageSender.setHttpClient(createHttpClient(createHttpClientConnectionManager()));

        return httpComponentsMessageSender;
    }

    private PoolingHttpClientConnectionManager createHttpClientConnectionManager() {
        var poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
        poolingHttpClientConnectionManager.setMaxTotal(20);
        poolingHttpClientConnectionManager.setDefaultMaxPerRoute(2);

        return poolingHttpClientConnectionManager;
    }

    private CloseableHttpClient createHttpClient(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
        return HttpClients.custom()
                .addInterceptorFirst(new HttpComponentsMessageSender.RemoveSoapHeadersInterceptor())
                .setConnectionManager(poolingHttpClientConnectionManager)
                .evictExpiredConnections()
                .evictIdleConnections(40, TimeUnit.SECONDS)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>트러블 슈팅</category>
      <category>connection reset</category>
      <category>ConnectionExpiredException</category>
      <category>could not send message</category>
      <category>httpclient</category>
      <category>SOAP</category>
      <author>infitry</author>
      <guid isPermaLink="true">https://infitry.tistory.com/74</guid>
      <comments>https://infitry.tistory.com/74#entry74comment</comments>
      <pubDate>Sun, 23 Jul 2023 15:16:10 +0900</pubDate>
    </item>
  </channel>
</rss>