본문 바로가기
프로그래밍/Spring Boot

@Transactional 어노테이션의 이해

by 카프카뮈 2021. 6. 15.

나는 보통 서비스 코드에 @Transactional 어노테이션을 활용해준다.

그런데 사실 뜻도 잘 모르고 좋다고 그래서 쓴거라...지나고 보니 정확히 설명하기가 어려웠다.

그런고로, 해당 어노테이션의 작동 원리부터 사용 예까지 조금 정리해보려고 한다.

 

내가 이해하려고 쓴 글인지라 많이 엉성하다.

이해가 가지 않거나 빠진 내용이 보이거나 한다면, 꼭 댓글 부탁드립니다.


트랜잭션이란?

데이터베이스 트랜잭션은 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위이다.

여기서 단위라는 말을 사용했는데, 쉽게 말하면 더 이상 쪼개질 수 없는 최소의 연산이라는 의미가 된다.

 

예를 들어보자. 만약 내가 쇼핑 앱을 켜서 상품을 구매하려고 한다.

그런데 내가 결제를 하는 짧은 시간 사이에 아래와 같은 일이 벌어지면 어떨까?

  • 해당 판매자가 상품의 가격을 바꿔버려서, 잘못된 금액이 결제됨
  • 같은 상품을 다른 사람도 구매해서, 상품 재고는 1개인데 2명에게 결제됨
  • 결제가 완료되기 직전에 네트워크가 끊겨서, 돈은 나갔지만 구매완료는 되지 않음

아무래도 황당할 수밖에는 없다. 

위의 예외적 상황을 막기 위해서, 다음과 같은 조치가 필요할 것이다.

  • 내가 결제중일 때에는 해당 상품의 정보를 바꿀 수 없게 함
  • 내가 결제중일 때에는 해당 상품을 다른 사람이 결제하지 못하게 함
  • 내 구매가 오류로 완료되지 않았다면, 결제된 금액을 환불 처리함

위의 조치사항을 좀 더 간략하게 정리하면, 아래와 같이 정리할 수 있다.

 

"결제는 다른 사람과 독립적으로 이루어지며, 과정 중에 다른 연산이 끼어들 수 없다.

오류가 생긴 경우 연산을 취소하고 원래대로 되돌린다. 성공할 경우 결과를 반영한다."

 

여기서 결제는 트랜잭션의 예시로 든 것이다. 트랜잭션 역시, 위의 원칙을 바탕으로 한다.

그래서 어떤 연산에 트랜잭션이 보장된다면, DB에서 의도치 않은 값이 저장되거나 조회되는 것을 막을 수 있다.

트랜잭션의 원칙을 더 정확하게 알고싶다면, ACID에 대한 위키 문서를 참고.

 

ACID - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 다른 뜻에 대해서는 애시드 문서를 참고하십시오. ACID(원자성, 일관성, 고립성, 지속성)는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한

ko.wikipedia.org


@Transactional 어노테이션

@Transactional은 클래스나 메서드에 붙여줄 경우, 해당 범위 내 메서드가 트랜잭션이 되도록 보장해준다.

선언적 트랜잭션이라고도 하는데, 직접 객체를 만들 필요 없이 선언만으로도 관리를 용이하게 해주기 때문. 

특히나 SpringBoot에서는 선언적 트랜잭션에 필요한 여러 설정이 이미 되어있는 탓에, 더 쉽게 사용할 수 있다.

 

예시로 아래 코드를 보자. 도서를 Repository를 통해 가져오는 서비스 레이어의 코드이다.

@RequiredArgsConstructor
@Service
public class BookService {
    private final BookRepository bookRepository;

    @Transactional(readOnly = true)
    public List<BookResponseServiceDto> getBooks() {
        return bookRepository.findAll()
                .stream()
                .map(BookResponseServiceDto::new)
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public BookResponseServiceDto getBook(Long bookId) {
        final Book book = bookRepository.findById(bookId)
                .orElseThrow(() -> new BookNotFoundException(bookId));

        return new BookResponseServiceDto(book);
    }
}

@Transactional 어노테이션을 getBooks()와 getBook() 메서드에 작성해 주었다.

그 결과로, 만약 getBook() 메서드가 실행될 경우 해당 메서드는 아래의 속성을 가진다.

  • 연산이 고립되어, 다른 연산과의 혼선으로 인해 잘못된 값을 가져오는 경우가 방지된다.
  • 연산의 원자성이 보장되어, 연산이 도중에 실패할 경우 변경사항이 Commit되지 않는다.

위의 속성이 보장되기 때문에, 해당 메서드를 실행하는 도중 메서드 값을 수정/삭제하려는 시도가 들어와도 값의 신뢰성이 보장된다. 또한, 연산 도중 오류가 발생해도 rollback해서 DB에 해당 결과가 반영되지 않도록 할 수 있다. (위의 코드가 조회 코드가 와닿지 않을 수 있지만, 생성/수정/삭제 도중 오류가 발생한다고 생각해보자!!)


@Transactional의 작동 원리와 흐름

그렇다면 @Transactional이 붙은 메서드를 호출할 경우, 우리 코드에는 어떤 일이 벌어질까?

 

@Transactional이 클래스 내지 메서드게 붙을 때, Spring은 해당 메서드에 대한 프록시를 만든다.

프록시 패턴은 디자인 패턴 중 하나로, 어떤 코드를 감싸면서 추가적인 연산을 수행하도록 강제하는 방법이다.

 

트랜잭션의 경우, 트랜잭션의 시작과 연산 종료시의 커밋 과정이 필요하므로, 프록시를 생성해 해당 메서드의 앞뒤에 트랜잭션의 시작과 끝을 추가하는 것이다.

이러한 로직은 AOP에 바탕을 두고 설계되었기 때문에, 이후 설명에서 해당 프록시는 트랜잭션 AOP로 명칭하겠다.

더보기

유의사항: 호출의 문제

What's important to keep in mind is that, if the transactional bean is implementing an interface, by default the proxy will be a Java Dynamic Proxy. This means that only external method calls that come in through the proxy will be intercepted. Any self-invocation calls will not start any transaction, even if the method has the @Transactional annotation.

출처 : https://www.baeldung.com/transaction-configuration-with-jpa-and-spring

 

스프링이 만들어주는 proxy는 동적으로 생성되며, 프록시를 통해 들어오는 외부 메서드 호출은 차단된다.

하지만, 내가 자체적으로 다른 메서드를 호출하는 경우, 해당 메서드에는 트랜젝션이 적용되지 않을수도 있다.

 

이에 대해 좋은 글이 있어, 인용하려고 한다.

https://mommoo.tistory.com/92

 

[Spring] @Transactional 사용시 주의해야할 점

Spring @Transactional 주의점 안녕하세요. 오늘은 Spring 의 @Transactional 어노테이션을 사용할 때, 주의점에 대해 포스팅합니다. Spring 에서 JPA 기술을 쓸 때 빼놓을 수 없는 기능중 하나는 @Transa.

mommoo.tistory.com

 

또한, 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.

서비스 클래스에서 @Transactional을 사용할 경우, 해당 코드 내의 메서드를 호출할 때 영속성 컨텍스트가 생긴다는 뜻이다. 영속성 컨텍스트는 트랜잭션 AOP가 트랜잭션을 시작할 때 생겨나고, 메서드가 종료되어 트랜잭션 AOP가 트랜잭션을 커밋할 경우 영속성 컨텍스트가 flush되면서 해당 내용이 반영된다. 이후 영속성 컨텍스트 역시 종료되는 것이다.

김영한님의 [자바ORM표준 JPA프로그래밍] 579p 인용

이러한 방식으로 영속성 컨텍스트를 관리해 주기 때문에, @Transactional을 쓸 경우 트랜잭션의 원칙을 정확히 지킬 수 있다.

또한, 아래의 원칙 역시 유의해야 한다.

  • 만약 같은 트랜잭션 내에서 여러 EntityManager를 쓰더라도, 이는 같은 영속성 컨텍스트를 사용한다.
  • 같은 EntityManager를 쓰더라도, 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.

아직 남은 문제들

이 글을 통해, 트랜잭션이 무엇인지, @Transactional이 무엇인지 알아보았다.

또한 @Transactional 호출 시 어떤 동작이 생기는지, 어떻게 관리되는지에 대해서도 알 수 있었다.

다만 아직 관련하여 다루지 못한 몇 가지 주제가 있다.

 


참고한 출처를 남긴다.

Baeldung: Transactional with jpa and spring: Baeldung 최고!! 정말 필요한 원리를 깔끔히 설명해준다.

프록시 패턴 & 스프링 AOP : 깔끔하게 정리를 잘해주셔서 프록시 패턴을 빠르게 이해할 수 있었다.

추가로, 김영한님의 자바 ORM 표준 JPA 프로그래밍 책도 많이 참고했다. 없으면 안되는 바이블..

반응형

댓글