spring-트랜잭션-이해

아엘·2024년 3월 21일
0

spring framework

목록 보기
4/5

프로젝트 생성

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.9'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
    // 테스트에서 lombok을 사용하기 위한 의존성 추가
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}

tasks.named('bootBuildImage') {
    builder = 'paketobuildpacks/builder-jammy-base:latest'
}

tasks.named('test') {
    useJUnitPlatform()
}

테스트 코드

package com.example.springtx.apply;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import static org.assertj.core.api.Assertions.assertThat;

@Slf4j
@SpringBootTest
public class TxBasicTest {

    @Autowired
    BasicService basicService;

    @TestConfiguration
    static class TxApplyBasicConfig {
        @Bean
        BasicService basicService() {
            return new BasicService();
        }
    }

    @Slf4j
    static class BasicService {

        @Transactional
        public void tx() {
            log.info("call tx");
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("txActive: {}", txActive);
        }

        public void nonTx() {
            log.info("call nonTx");
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("txActive: {}", txActive);
        }

    }

    @Test
    void proxyCheck() {
        log.info("aop class= {}", basicService.getClass().getName());
        assertThat(AopUtils.isAopProxy(basicService)).isTrue();
    }

    @Test
    void txTest() {
        basicService.tx();
        basicService.nonTx();
    }
}

로그

2024-03-21T10:58:14.706+09:00  INFO 51573 --- [    Test worker] c.e.s.apply.TxBasicTest$BasicService     : call tx
2024-03-21T10:58:14.706+09:00  INFO 51573 --- [    Test worker] c.e.s.apply.TxBasicTest$BasicService     : txActive: true
2024-03-21T10:58:14.707+09:00  INFO 51573 --- [    Test worker] c.e.s.apply.TxBasicTest$BasicService     : call nonTx
2024-03-21T10:58:14.707+09:00  INFO 51573 --- [    Test worker] c.e.s.apply.TxBasicTest$BasicService     : txActive: false

@Transactional 애노테이션이 클래스나 메서드에 하나라도 있으면 트랜잭션 AOP는 프록시 객체를 만들어서 IoC Container에 등록합니다. 실제 basicService 객체 대신에 프록시인 basicService$$CGLIB를 스프링 빈에 등록합니다.
그리고 프록시는 내부에 실제 basicService를 참조하게 됩니다. 여기서 핵심은 실제 객체 대신에 프록시가 스프링 컨테이너에 등록되었다는 점입니다.

txBasicTest는 프록시 객체에 요청하게 되고 프록시 객체가 프록시에 수행할 로직 사이에 실제 서비스의 call부분을 호출하게 됩니다. 프록시 객체는 실제 서비스를 상속한 서비스이기 때문에 txBasicTest에서 호출할때에는 프록시 객체에 요청하는지 모릅니다.

트랜잭션 로깅을 확인하기 위해 application.yml에 설정합니다.

logging:
  level:
    org.springframework.transaction.interceptor: TRACE

트랜잭션 로깅후 로그

2024-03-21T11:07:30.686+09:00 TRACE 51808 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.springtx.apply.TxBasicTest$BasicService.tx]
2024-03-21T11:07:30.686+09:00  INFO 51808 --- [    Test worker] c.e.s.apply.TxBasicTest$BasicService     : call tx
2024-03-21T11:07:30.686+09:00  INFO 51808 --- [    Test worker] c.e.s.apply.TxBasicTest$BasicService     : txActive: true
2024-03-21T11:07:30.686+09:00 TRACE 51808 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.springtx.apply.TxBasicTest$BasicService.tx]
2024-03-21T11:07:30.687+09:00  INFO 51808 --- [    Test worker] c.e.s.apply.TxBasicTest$BasicService     : call nonTx
2024-03-21T11:07:30.687+09:00  INFO 51808 --- [    Test worker] c.e.s.apply.TxBasicTest$BasicService     : txActive: false

Q: @Transactional 애노테이션을 붙여서 프록시 서비스가 생성되었으면 nonTx 호출할때도 프록시 서비스에서 트랜잭션 적용되는거 아니에요?

txBasicTest에서 basicService.tx()를 호출하게 되면 프록시 서비스 내에서 트랜잭션 대상인지 확인하고, 트랜잭션 대상이면 트랜잭션을 활성화해줍니다.
basicService.nonTx()는 트랜잭션 대상이 아니기 때문에 트랜잭션 활성화하지 않습니다.

트랜잭션 주의 사항

테스트 코드

package com.example.springtx.apply;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import static org.assertj.core.api.Assertions.assertThat;

@Slf4j
@SpringBootTest
public class InternalCallV1Test {

    @Autowired CallService callService;

    @TestConfiguration
    static class InternalCallV1TestConfig {

        @Bean
        CallService callService() {
            return new CallService();
        }
    }

    static class CallService {
        public void external() {
            log.info("call external");
            printTxInfo();
            internal();
        }

        @Transactional
        public void internal() {
            log.info("call internal");
            printTxInfo();
        }

        private void printTxInfo() {
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("txActive: {}", txActive);
            boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
            log.info("readOnly: {}", readOnly);
        }
    }

    @Test
    void proxyCheck() {
        log.info("aop class= {}", callService.getClass().getName());
        assertThat(AopUtils.isAopProxy(callService)).isTrue();
    }

    @Test
    void callInternal() {
        callService.internal();
    }
}

callInternal test

    @Test
    void callInternal() {
        callService.internal();
    }

callInternal test 로그

2024-03-21T11:18:43.576+09:00 TRACE 52078 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.springtx.apply.InternalCallV1Test$CallService.internal]
2024-03-21T11:18:43.577+09:00  INFO 52078 --- [    Test worker] c.e.springtx.apply.InternalCallV1Test    : call internal
2024-03-21T11:18:43.577+09:00  INFO 52078 --- [    Test worker] c.e.springtx.apply.InternalCallV1Test    : txActive: true
2024-03-21T11:18:43.577+09:00  INFO 52078 --- [    Test worker] c.e.springtx.apply.InternalCallV1Test    : readOnly: false
2024-03-21T11:18:43.577+09:00 TRACE 52078 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.springtx.apply.InternalCallV1Test$CallService.internal]

callExternal test

    @Test
    void callExternal() {
        callService.external();
    }

callExternal test 로그

2024-03-21T11:19:59.290+09:00  INFO 52106 --- [    Test worker] c.e.springtx.apply.InternalCallV1Test    : call external
2024-03-21T11:19:59.290+09:00  INFO 52106 --- [    Test worker] c.e.springtx.apply.InternalCallV1Test    : txActive: false
2024-03-21T11:19:59.290+09:00  INFO 52106 --- [    Test worker] c.e.springtx.apply.InternalCallV1Test    : readOnly: false
2024-03-21T11:19:59.291+09:00  INFO 52106 --- [    Test worker] c.e.springtx.apply.InternalCallV1Test    : call internal
2024-03-21T11:19:59.291+09:00  INFO 52106 --- [    Test worker] c.e.springtx.apply.InternalCallV1Test    : txActive: false
2024-03-21T11:19:59.291+09:00  INFO 52106 --- [    Test worker] c.e.springtx.apply.InternalCallV1Test    : readOnly: false

callInternal은 트랜잭션이 적용되었고,
callExternal은 트랜잭션이 적용안된것을 로그를 통해 파악했습니다.

callExternal 트랜잭션 적용 안되는 원인파악

@Transcational 이 하나라도 있으면 트랜잭션 프록시 객체가 만들어집니다. 그리고 callService 빈을 주입 받으면 트랜잭션 프록시 객체가 대신 주입됩니다.

class에 애노테이션이 없고, internal 메서드에 @Transactional이 붙었고, external 메서드에는 @Transactionl이 붙어있지 않습니다.
이 경우에는 프록시 객체에서 트랜잭션 적용이 안된상태로 실제 callService가 호출되었습니다.
추가로 external() 내부에서 internal()를 호출합니다. 이미 트랜잭션이 적용 안된 상태로 실제 callService에서 호출되기 때문에 interal()에서도 트랜잭션 적용이 안됩니다.

java에서는 메서드 앞에 별도의 참조가 없으면 this라는 뜻으로 자기자신의 인스턴스를 가리킵니다.

callExternal에서 트랜잭션 적용 안되는 이슈 해결

테스트 코드

package com.example.springtx.apply;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import static org.assertj.core.api.Assertions.assertThat;

@Slf4j
@SpringBootTest
public class InternalCallV2Test {
    @Autowired
    CallService callService;

    @TestConfiguration
    static class InternalCallV1TestConfig {

        @Bean
        CallService callService() {
            return new CallService(internalService());
        }

        @Bean
        InternalService internalService() {
            return new InternalService();
        }
    }

    @RequiredArgsConstructor
    static class CallService {

        private final InternalService internalService;

        public void external() {
            log.info("call external");
            printTxInfo();
            internalService.internal();
        }

        private void printTxInfo() {
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("txActive: {}", txActive);
            boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
            log.info("readOnly: {}", readOnly);
        }
    }

    static class InternalService {
        @Transactional
        public void internal() {
            log.info("call internal");
            printTxInfo();
        }

        private void printTxInfo() {
            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("txActive: {}", txActive);
            boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
            log.info("readOnly: {}", readOnly);
        }
    }

    @Test
    void proxyCheck() {
        log.info("aop class= {}", callService.getClass().getName());
        assertThat(AopUtils.isAopProxy(callService)).isTrue();
    }


    @Test
    void callExternalV2() {
        callService.external();
    }
}

테스트 코드 로그

2024-03-21T11:34:49.711+09:00  INFO 52506 --- [    Test worker] c.e.springtx.apply.InternalCallV2Test    : call external
2024-03-21T11:34:49.711+09:00  INFO 52506 --- [    Test worker] c.e.springtx.apply.InternalCallV2Test    : txActive: false
2024-03-21T11:34:49.711+09:00  INFO 52506 --- [    Test worker] c.e.springtx.apply.InternalCallV2Test    : readOnly: false
2024-03-21T11:34:49.728+09:00 TRACE 52506 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.springtx.apply.InternalCallV2Test$InternalService.internal]
2024-03-21T11:34:49.728+09:00  INFO 52506 --- [    Test worker] c.e.springtx.apply.InternalCallV2Test    : call internal
2024-03-21T11:34:49.728+09:00  INFO 52506 --- [    Test worker] c.e.springtx.apply.InternalCallV2Test    : txActive: true
2024-03-21T11:34:49.728+09:00  INFO 52506 --- [    Test worker] c.e.springtx.apply.InternalCallV2Test    : readOnly: false
2024-03-21T11:34:49.728+09:00 TRACE 52506 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.springtx.apply.InternalCallV2Test$InternalService.internal]

참고

profile
하루 하나씩

0개의 댓글