본문 바로가기

데이터베이스

@Transactional 동작원리(+readOnly=true)

@Transactional의 동작 방식과 readOnly=true 설정에 대해 알아보도록 하겠습니다! 혹시 모르신다면 이번 포스팅에서 한번 알아가보시는 걸 추천드립니다 :)

 

그리고 트랜잭션에 대한 개념 및 ACID에 대해 처음 들어 보신다면 아래의 포스팅에서 트랜잭션 개념 부분을 참고하신 후 포스팅을 읽어 주시면 감사하겠습니다.

 

https://wookjongbackend.tistory.com/39

 

트랜잭션의 개념과 락(Lock)과 동시성에 대해 알아보자!

트랜잭션이란?? 트랜잭션이란, 데이터베이스의 상태를 변경시키기 위해 수행하는 작업 단위 또는 한꺼번에 수행되어야 할 일련의 연산들을 의미합니다. 주로 UPDATE, INSERT, DELETE와 같은 데이터베

wookjongbackend.tistory.com


AOP(Aspect Oriented Programming) + Proxy Pattern

@Transactional 동작 원리에 대해 알기 전 간단히 AOP에 대해 간단히 알아보겠습니다!

객체 지향 프로그래밍(OOP)은 어플리케이션을 설계할때 책임과 관심사에 따라 클래스를 분리합니다. 이는 어플리케이션의 한 부분에서 변경이 발생했을 때, 그 파급 효과가 시스템의 전체로 퍼져나가는 정도를 낮추기 위해서입니다(응집도를 높이고, 결합도는 낮추고)

 

하지만 이러한 OOP의 방식을 충실히 따르더라도, 아쉬운 점이 존재합니다. 위 사진 처럼, 여러 클래스에 로깅이나 보안 및 트랜잭션 등 공통된 기능들이 흩어져 존재한다는 점입니다!

이렇게 주요 로직은 아니지만, 반복적으로 여러 곳에서 쓰이는 것들을 흩어진 관심사(Cross Cutting Concerns)라고 합니다.

 

이때 흩어진 관심사를 별도의 클래스로 모듈화하여 위의 문제들을 해결하고, 결과적으로 OOP를 더욱 잘 지킬 수 있도록 도움을 주는 것이 AOP입니다.

 

Spring AOP는 기본적으로 프록시 패턴을 사용하여 동작합니다. 이때 프록시 패턴이란 특정 객체를 대신해서 그 객체의 기능을 수행하거나, 추가적인 작업을 수행하는 디자인 패턴입니다.

 

그렇다면 Spring은 왜 Target(부가 기능을 제공할 대상)을 직접 참조하지 않고 프록시 방식을 사용할까요??

 

이는 비즈니스 로직과 부가 기능(Aspect)을 깔끔하게 분리하기 위해서입니다!

 

만약 프록시 객체를 사용하지 않으면, 비즈니스 로직을 구현하는 Target 객체 안에 직접 부가 기능을 호출해야 합니다. 이렇게 되면 비즈니스 로직과 부가 기능이 뒤섞이게 되어 코드의 가독성이 떨어질 뿐만 아니라 유지보수가 어려워집니다.

 

반면, 프록시 객체를 사용하면, 클라이언트는 비즈니스 로직을 담당하는 Target 객체를 직접 호출하는 것처럼 보이지만 실제로는 프록시 객체를 통해 접근하게 됩니다. 이때 프록시 객체는 부가 기능을 실행한 뒤 Target 객체의 메서드를 호출하므로, 비즈니스 로직과 부가기능이 깔끔하게 분리되는 것이죠. 즉 코드의 가독성, 유지보수성 뿐 아니라 Target 객체는 순수한 비즈니스 로직에만 집중할 수 있는 것입니다 :)

 

이러한 프록시 클래스는 Target 클래스 혹은 그 상위 인터페이스를 구현 하는 방식으로 생성합니다.


@Transactional의 작동 원리 및 흐름

@Transactional 어노테이션은 스프링 프레임워크에서 제공하는 선언적 트랜잭션 관리 기능으로, 해당 어노테이션이 붙은 메서드가 호출되면, 스프링은 자동으로 트랜잭션을 시작하고 종료 시에 커밋하거나 롤백합니다. 

 

이러한 과정은 프록시 패턴을 사용해 구현되며, AOP의 개념에 기반을 두고 있습니다.

 

그렇다면, @Transactional 어노테이션이 붙은 메서드를 호출할때, 스프링에서 어떤 일이 벌어지는지 작동 원리와 흐름을 자세히 알아보겠습니다!

 

1. 프록시 객체의 생성

먼저, @Transactional이 선언된 클래스나 메서드가 있을 경우, 스프링은 이에 대한 프록시 객체를 생성합니다. 이 프록시 객체는 대상 객체를 감싸면서, 해당 메서드 호출 전 후에 필요한 트랜잭션 처리를 수행하는 역할을 합니다.

 

2. 트랜잭션의 시작

프록시 객체를 통해 @Transactional이 붙은 메서드가 호출되면, 트랜잭션 관리자(ex: PlatformTransactionManager)는 새로운 트랜잭션을 시작합니다. 이때 트랜잭션의 속성은 @Transactional 어노테이션이 설정한 속성에 따라 결정됩니다.

 

3. 영속성 컨텍스트의 생성 및 연결

트랜잭션이 시작되면, JPA를 사용할 경우 영속성 컨텍스트가 생성되고, 이와 트랜잭션을 연결합니다. 이렇게 만들어진 영속성 컨텍스트는 트랜잭션 범위의 영속성 컨텍스트라고 합니다. 즉 트랜잭션이 시작할 때 생성되고 트랜잭션이 종료될 때 까지 유지됩니다.

 

4. 비즈니스 로직 실행

트랜잭션이 시작되고 영속성 컨텍스트가 준비되면, 실제 비즈니스 로직이 실행됩니다.

 

5. 트랜잭션의 커밋 또는 롤백

비즈니스 로직의 실행이 정상적으로 끝나면, 트랜잭션 관리자는 트랜잭션을 커밋하고 영속성 컨텍스트를 종료합니다.

이 과정에서 Dirty Checking(밑에서 설명하는 개념입니다)이 일어나 변경된 도메인 객체에 대한 SQL이 DB에 반영됩니다.

 

만약 비즈니스 로직 실행 도중 예외가 발생하면, 트랜잭션은 롤백되고 해당 트랜잭션 범위의 영속성 컨텍스트 또한 종료됩니다.


@Transactional 사용 시 유의할 점!

1. private은 트랜잭션 처리를 할 수 없다.

@Transactional 어노테이션이 적용되는 메서드나 클래스는 Spring Framework에서 동작할 때 프록시 패턴을 사용하여 트랜잭션 처리를 합니다. 이러한 프록시 객체는 실제 타겟 객체나 그것의 인터페이스를 상속 받아 생성되며, 원본 서비스 객체의 메서드를 대신 호출하면서 트랜잭션 관련 로직을 수행합니다.

 

만약 @Transactional이 붙은 메서드가 private 접근 제한자를 가진다면, 프록시 객체가 해당 메서드에 접근할 수 없게 됩니다. 

 

결과적으로, 트랜잭션을 적용하고자 하는 메서드나 클래스의 접근 제한자는 private 보다 넓은 범위(public, protected 등)로 설정해야 프록시 객체가 올바르게 접근하여 트랜잭션 처리를 할 수 있습니다.

 

2. 외부 메서드, 내부 메서드에 대한 @Transactional 적용 결과 

@SpringBootTest
class SimpleServiceTest {

    @Autowired
    private SimpleService simpleService;

    @Test
    void transaction_test() {
        simpleService.outerMethod();
    }
}

위와 같이 외부에서 outerMethod를 호출하고, outerMethod에서 innerMethod를 호출한다고 가정해보겠습니다.

 

외부 및 내부 메서드, 둘 다에 @Transactional을 적용한 경우

@Slf4j
@Service
public class SimpleService {

    @Transactional
    public void outerMethod() {
        log.info("==== outerMethod start ====");
        log.info("==== outerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        innerMethod();
        log.info("==== outerMethod end ====");
    }

    @Transactional
    public void innerMethod() {
        log.info("==== innerMethod start ====");
        log.info("==== innerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        log.info("==== innerMethod end ====");
    }
}
==== outerMethod start ====
==== outerMethod transaction Active : true
==== innerMethod start ====
==== innerMethod transaction Active : true
==== innerMethod end ====
==== outerMethod end ====

이때는 outer,inner 메서드 모두 트랜잭션이 적용됩니다. 스프링에서 트랜잭션 전파(Transactional Propagation)의 디폴트 값은 Required 입니다. Required 타입의 경우 진행중인 트랜잭션 내부에 새로운 트랜잭션이 들어온다면 기존 트랜잭션에 참여하는 전파 방식입니다.

 

따라서 outerMethod의 트랜잭션에 innerMethod의 트랜잭션이 참여하는 구조가 됩니다.

 

외부 메서드에만 @Transactional을 적용한 경우

 

@Slf4j
@Service
public class SimpleService {

    @Transactional
    public void outerMethod() {
        log.info("==== outerMethod start ====");
        log.info("==== outerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        innerMethod();
        log.info("==== outerMethod end ====");
    }

    public void innerMethod() {
        log.info("==== innerMethod start ====");
        log.info("==== innerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        log.info("==== innerMethod end ====");
    }
}
==== outerMethod start ====
==== outerMethod transaction Active : true
==== innerMethod start ====
==== innerMethod transaction Active : true
==== innerMethod end ====
==== outerMethod end ====

이때도 마찬가지로, outer & inner 메서드 모두 트랜잭션이 적용됩니다.

OuterMethod는 @Transactional 어노테이션을 통해 트랜잭션이 시작되어있는 상태입니다. 이때 outerMethod가 종료되기 전(트랜잭션이 닫히기 전)에 innerMethod가 호출되었음으로, innerMethod는 outerMethod의 트랜잭션을 사용하게 됩니다.

 

내부 메서드에만 @Transactional을 적용한 경우

 

@Slf4j
@Service
public class SimpleService {

    public void outerMethod() {
        log.info("==== outerMethod start ====");
        log.info("==== outerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        innerMethod();
        log.info("==== outerMethod end ====");
    }

    @Transactional
    public void innerMethod() {
        log.info("==== innerMethod start ====");
        log.info("==== innerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        log.info("==== innerMethod end ====");
    }
}
==== outerMethod start ====
==== outerMethod transaction Active : false
==== innerMethod start ====
==== innerMethod transaction Active : false
==== innerMethod end ====
==== outerMethod end ====

 위 경우엔 outer,inner 메서드 모두 트랜잭션이 적용되지 않습니다. 왜그럴까요??

 

먼저, SimpleService는 스프링 빈으로 등록되면서, @Transactional이 붙은 메서드(InnerMethod)를 포함하고 있기 때문에 스프링은 이에 대한 프록시 객체를 생성합니다. 이때 프록시 객체는 아래와 같은 형식으로 생성될 것 입니다(예시)

 

public class SimpleServiceProxy extends SimpleService {

    private TransactionManager txManager;

    @Override
    public void outerMethod() {
        // 여기서는 특별한 로직 없이 부모 클래스의 outerMethod를 그대로 호출
        super.outerMethod();
    }

    @Override
    @Transactional
    public void innerMethod() {
        try {
            // 트랜잭션 시작
            txManager.beginTransaction();
            
            // 원래 SimpleService의 innerMethod를 호출
            super.innerMethod();
            
            // 트랜잭션 커밋
            txManager.commit();
        } catch (Exception e) {
            // 예외 발생 시, 트랜잭션 롤백
            txManager.rollback();
            throw e;
        }
    }
}

이제 SimpleServiceTest에서 simpleService.outerMethod()를 호출하면 프록시 객체의 outerMethod가 실행됩니다.

하지만 outerMethod에는 @Transactional이 붙어 있지 않기에, 프록시 객체에서 특별 추가 로직 없이 원본 outerMethod가 실행됩니다.

 

여기서 주의할 점은 outerMethod는 다시 innerMethod를 호출합니다. 즉 여기서 호출하는 innerMethod는 원본 SimpleService의 innerMethod 입니다. 즉 프록시 객체를 거치지 않고 바로 원본 메서드를 호출하게 되므로, 프록시 객체에 추가된 트랜잭션 시작/종료 로직이 실행되지 않게 됩니다.

 

정리하자면 outMethod에서 직접적으로 innerMethod를 호출할때 프록시 객체를 거치지 않고 원본 SimpleService의 innerMethod를 호출하게 되므로 트랜잭션 로직이 적용되지 않는 것입니다!

 

따라서 내부 메서드에만 트랜잭션을 적용해야 하는 상황이라면, 별도의 클래스로 내부 메서드를 빼내, 해당 클래스의 프록시가 올바르게 트랜잭션 관련 코드를 적용할 수 있도록 처리해야합니다.


@ readOnly=true 옵션

혹시 @Transactional 어노테이션에 readOnly = true 옵션을사용해보셨나요???

 

저와 함께 어떨 때 사용하는지!, 그리고 사용 시 어떤 이점을 가지는 지에 대해 알아보겠습니다 :)

 

readOnly=true를 사용했을때 성능이 좋아지는 이유는, JPA의 영속성 컨텍스트가 수행하는 Dirty Checking과 관련이 있습니다.

 

코드 예시

public SellerItemResponseDto addStock(Long sellerId, Long itemId, int addNum) {
    Item item = itemRepository.findByIdAndSellerId(itemId, sellerId)
        .orElseThrow(() -> new BusinessException(ErrorCode.ITEM_NOT_MATCH));

    Stock stock = stockRepository.findByIdForUpdate(item.getStock().getId())
        .orElseThrow(() -> new BusinessException(ErrorCode.STOCK_NOT_FOUND));

    stock.setQuantity(stock.getQuantity() + addNum);

//    findById 사용시 해당 ID를 가진 엔티티가 검색되고, 이 엔티티는 자동으로 영속성 컨텍스트에 포함됨
//    이렇게 영속화된 Stock을 변경하면, 이 변경사항은 영속성 컨텍스트에 의해 추적되고(더티 체킹), 트랜잭션 종료될때 DB에 반영
//    stockRepository.save(stock);

    return SellerItemResponseDto.of(item);
  }

영속성 컨텍스트는 Entity 조회 시 초기 상태에 대한 SnapShot을 저장합니다.

 

그리고 트랜잭션이 commit 될때, 초기 상태의 정보를 가지는 SnapShot과 Entity의 현재 상태를 비교하여 변경된 내용에 대해 update query를 생성해 쓰기 지연 SQL 저장소에 저장합니다.

 

이후, 일괄적으로 쓰기 지연 저장소에 저장되어 있는 query를 flush하고 데이터베이스의 트랜잭션을 Commit 함으로써 우리가 update와 같은 메서드를 사용하지 않고도 Entity의 수정이 이루어집니다. 이를 Dirty Checking이라고 합니다!

 

readOnly = true 설정이 있다면, 스프링 프레임워크는 JPA의 세션 플러시 모드를 MANUAL로 설정합니다.

MANUAL 모드는 이름 그대로 사용자가 수동으로 flush()를 호출해야만, 영속성 컨텍스트 변경 내용들이 데이터베이스에 반영되는 방식입니다!

 

따라서 이 실정이 활성화된 트랜잭션 내에서는 flush()가 호출되지 않는 한, DB에 데이터베이스에 반영되지 않기 떄문에 데이터의 일관성을 유지하는데 도움이 됩니다.

 

또한 readOnly = true 설정이 활성화된 트랜잭션 내에서는, 조회한 엔티티에 대해 변경 감지를 위한 스냅샷을 생성하지 않습니다.

 

엔티티의 변경을 감지하기 위해서는 원본 엔티티의 상태를 어딘가에 저장해 두어야 하는데, 이것이 바로 스냅샷입니다.

그런데 readOnly=true가 설정된 트랜잭션에서는 이러한 스냅샷을 생성하지 않기 때문에, 이로 인해 발생할 수 있는 메모리 부하를 줄일 수 있습니다.

 

즉 조회용 메서드에, readOnly=true 설정은 데이터의 일관성을 보장하고, 메모리 사용량을 줄이는 데 도움이 되므로 성능 향상에 기여하게 됩니다.

참고

https://junny00.tistory.com/entry/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%9D%B4%EC%A0%90

https://kafcamus.tistory.com/30

https://velog.io/@ann0905/AOP%EC%99%80-Transactional%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC

https://tecoble.techcourse.co.kr/post/2021-06-25-aop-transaction/

https://coding-factory.tistory.com/711

https://www.codesenior.com/en/tutorial/Aspect-Oriented-Programming-AOP

https://hungseong.tistory.com/81

 

틀린 내용이 있다면 댓글 부탁드리겠습니다 :) 확인 후 곧바로 수정하도록 하겠습니다!