[Spring / TIL] @Transactional(readOnly=true) 가 꼭 필요한가?

resilient

·

2022. 2. 19. 22:14

728x90
반응형

Spring 학습을 위해 프로젝트를 진행하던 도중 조회한 값을 return 해주는 메소드에 당연하게 @Transactional(readOnly=true) 어노테이션을 사용했습니다.

막연하게 롤백(rollback) 때 사용하니까 @Transactional 어노테이션을 사용하긴 하지만, C, U, D 메소드가 아닌 읽어 오는 메소드는 @Transactional(readOnly=true)를 왜 사용하지? 아예 @Transactional 어노테이션을 안 붙이면 되지 않나?라는 궁금증이 들었고, 이번 기회에 확실하게 알아보려고 합니다.

 

트랜잭션(Transaction) 이란?

 

데이터베이스의 상태를 변경하고자 할 때, 한번에 수행되어야 하는 연산들을 의미합니다. 

 

Nest.js의 예를 먼저 들어보겠습니다. 

 

async unlike(productId: string, user: User) {
    if (!productId || !user) {
      throw new BadRequestException('productId or user not exist');
    }
    //connect생성
    const session = await this.connection.startSession();
    // Transaction시작 (begin)
    await session.withTransaction(async () => {
      const unlike = await this.userProductLikeModel.findOneAndDelete(
        {
          user: user._id,
          product: productId,
        },
        { session },
      );
      if (unlike) {
        const productUnlike = await this.productModel.findByIdAndUpdate(
          productId,
          {
            $inc: { user_likes: -1 },
          },
          { session },
        );
      }
    });
    // transaction 종료 (commit)
    session.endSession();
    return {};
  }

 

위 코드를 보면, 한번 메소드가 실행될 때 한 번에 수행되어야 하는 연산이 두 개가 있습니다. userProductLikeModel 엔티티에 접근해서 수정하는 연산과 productModel 엔티티에 접근해서 user_likes의 카운트를 낮춰주는 연산이 있죠. 

첫 번째 연산이 실행되고, 두번째 연산을 실행하던 도중 예외가 발생한다면 첫 번째 연산까지 취소를 해줘야 합니다. 그래야 불상사가 일어나지 않겠죠? 여기서 첫 번째 연산까지 취소해주는 행동을 롤백(rollback) 처리를 한다고 이야기합니다. 이어서 @Transactional 어노테이션으로 더 설명을 이어 나가보겠습니다.

 

자 다시 스프링으로 넘어와서!


위 코드처럼 트랜잭션을 만들고, begin, commit까지 완료하면 하나의 트랜잭션이 수행된다라고 말합니다.

 

 

@Transactional 어노테이션은 트랜잭션을 선언하고 begin, commit까지 자동으로 수행해줍니다. 당연히 예외 발생 시 rollback처리도 자동을 수행해주죠. 트랜잭션은 4가지의 성질을 가지고 있습니다. (트랜잭션 ACID 성질이라고도 하죠)

 

  • 원자성(Atomicity) : 트랜잭션은 아토믹해야합니다. 한 트랜잭션 내에서 실행한 작업들은 하나의 단위로 처리해서 모두 성공, 모두 실패가 되어야 합니다. 때문에 rollback 처리가 필수인 거죠.
  • 일관성(Consistency) : 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 합니다. 연산이 실행될 수 있는 환경들이 조건으로 주어지고, 이를 반드시 만족시켜야 하죠.
  • 격리성(Isolation) : 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 해야 합니다.
  • 영속성(Durability) : 성공적으로 완료(commit) 된 트랜잭션은 데이터베이스에 영구적으로 보존되어야 합니다. 완료 후에 컴퓨터가 꺼진다면 데이터는 이미 저장되었으므로 보존되고, 완료 전에 컴퓨터가 꺼진다면 데이터는 원자성 원칙을 따라, 트랜잭션 수행하기 전으로 돌아가죠.

 

네트워크 환경에서 ACID특성을 보장하는 것은 굉장히 어렵습니다. 연결이 끊길 수도 있고 두 사용자가 동시에 DB의 동일한 부분을 접근할 수도 있죠. 트랜잭션의 commit 여부를 각 사용자로부터 확인하기 위해 2단계 commit이 분산 트랜잭션에 적용됩니다. 이렇게 commit 여부를 확인하고 ACID특성을 유지하기 위해 사용하는 처리방법들은 다음에 살펴보도록 하겠습니다.

 

정리해보면, 스프링에서는 간단하게 어노테이션 방식으로 @Transactional을 메소드, 클래스, 인터페이스 위에 추가하여 사용하는 방식이 일반적입니다. 이 방식을 선언적 트랜잭션이라 부르며, 적용된 범위에서는 트랜잭션 기능이 포함된 프록시 객체가 생성되어 자동으로 commit 혹은 rollback을 진행해줍니다.

 

프로젝트코드에서 @Transactional(readOnly=true) 사용

 

그래서, @Transactional(readOnly=true)가 꼭 필요한가?

 

결론은 써주는 게 좋다입니다. 이유를 설명드리겠습니다.

 

첫 번째, 조회한 데이터를 return 한다고 해도 의도치 않게 데이터가 변경되는 일을 사전에 방지해줍니다.

 

두 번째, 해당 옵션인 경우 CUD 작업이 동작하지 않고, 스냅샷 저장, 변경 감지(dirty check)의 작업을 수행하지 않아 성능이 향상됩니다. 여기서 dirty checking은 상태 변경검사인데요, dirty란 상태의 변화가 생긴 정도라고 이해하면 될 것 같습니다.  이 부분은 영속성 컨텍스트와 연결되는데요, 다음 시간에 추가적으로 살펴보도록 하겠습니다.

JPA에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영해줍니다. JPA에서는 엔티티를 조회하면 해당 엔티티의 처음 조회 상태 그대로 스냅샷을 만들어놓습니다. 그리고 트랜잭션이 끝나는 시점에는 이 스냅샷과 비교해서 다른 점이 있다면 Update Query를 데이터베이스로 전달합니다. - 이동욱 님 블로그

 

세 번째, MySQL을 사용할 때 데이터가 날아가는 것을 방지하기 위해서 이중화 구성(master - Slave)을 하는 경우가 있는데 DB가 master와 slave로 나누어져 있다면 readOnly = true로 있는 경우에는 읽기 전용으로 master가 아닌 slave를 호출하게 됩니다. 즉, 상황에 따라 DB 서버의 부하를 줄이고 약간의 최적화를 할 수 있습니다.

 

마지막으로 @Transactional(readOnly=true) 어노테이션이 있다면 코드를 접하는 사람들이 직관적으로 보기에 해당 메서드는 READ에 대한 동작만 수행할 것이라고 예상합니다. 그리고 결과적으로도 읽기 동작만 수행이 되죠. 또한 이 어노테이션을 보고 누구나 한눈에 알아볼 수 있고, 신뢰성을 보장한다고 받아들입니다. 

 

 

정리

 

@Transactional(readOnly=true) 어노테이션이 있다면 성능뿐만 아니라 누구나에게 확실하게 의미 전달을 하는 것만으로도 해당 어노테이션 옵션은 중요하다고 느껴졌습니다. 때문에 해당 옵션에서 아무런 성능적 이점을 갖지 못한 경우라도 명시적으로 두고 사용을 하는 게 좋아 보입니다.

 

감사합니다.

 

 

 

Reference


https://jojoldu.tistory.com/415

https://junhyunny.github.io/spring-boot/jpa/junit/transactional-readonly/

 

반응형