2 분 소요

개념

트랜잭션

트랜잭션은 DB 상태를 변경시키는 작업 단위를 말한다. 데이터를 변경하는 작업이라고 보면 되는데, 그러한 로직을 담고 있는 함수를
스프링에서 처리하려면 해당 함수에 트랜젹선을 적용해줘야 한다. 다음처럼 적용할 수 있다.

@Transactional
public void save(User user) {
  userRepository.save(user);
}

트랜잭션과 프록시 패턴

@Transactional이 붙은 함수는 스프링 애플리케이션을 실행하는 시점에서 프록시 패턴으로 동작한다.
프록시 패턴을 설명하기 이전에, 어떤 함수를 트랜잭션 범위 내에서 처리되도록 보장하려면 사실 다음 내용을 적어줘야 한다.
트랜잭션을 적용해야하는 함수가 굉장히 많다면, begin(), commit()을 호출하도록 항상 적어줘야 하므로 굉장히 번거로울 것이다.
그 과정에서 누락하는 등의 실수가 발생할 수도 있다.

public void save(User user) {
  transaction.begin(); //트랜잭션 시작
  userRepository.save(user);
  transaction.commit(); //트랜잭션 종료 및 DB 반영
}

하지만 @Transactional을 선언함으로써 스프링이 이 역할을 대신해준다. 직접 트랜잭션 호출, 종료 함수를 개별 호출해줄 필요가
없어진 셈이다! 그니까 스프링이 save() 호출 시 begin(), commit()이 포함된 새로운 함수를 만들어서 호출해주게 된다.
이러한 과정을 프록시 패턴이라고 한다.

트랜잭션 전파

설명

스프링에서 @Transactional을 적용한 함수 A, B가 있다고 하자. A는 B를 호출하는 상황이다!
이때 A를 처음으로 호출하므로 트랜잭션이 시작되고, 생성된 트랜잭션은 B까지 이어진다. 이를 트랜잭션 전파라고 한다.
B는 A처럼 첫 호출이 아니고 A에서 시작된 트랜잭션을 사용하기 때문에 새로운 트랜잭션을 생성하지 않는다.

@Transactional
public void A() {
  B();
}

@Transactional
public void B() {
}

롤백

트랜잭션은 원자성(Atomicity)이라는 특징을 갖고있기 때문에, 진행하던 작업이 중간에 문제가 생겼을 경우 트랜잭션 시작 이전의
상태로 복구한다. 이를 롤백이라고 한다. (원자성 = All or Nothing!)

위의 예시처럼 A가 B를 호출하는 상황이면, A가 트랜잭션을 시작하므로 외부 트랜잭션, B는 내부 트랜잭션이라고 해보자.
외부/내부 트랜잭션은 하나의 트랜잭션을 공유하고 있다. 그니까 외부 트랜잭션, 내부 트랜잭션 둘중 하나에 문제가 생겨서 commit을
하지 못하는 경우 롤백을 수행해야 하는데, 하나의 트랜잭션을 공유하고 있으므로 다른 트랜잭션은 작업이 완료됐다고 하더라도 전체
트랜잭션은 문제가 생긴거니까 모두 롤백을 수행한다. 한쪽만 commit하지 않는다!

내부 트랜잭션에 문제가 생기면 트랜잭션 매니저에 rollbackOnly를 마킹하고, 외부 트랜잭션에서 작업을 마치고 commit하기
전에 rollbackOnly를 확인하다. 이 값이 true면 전체 트랜잭션을 롤백한다.

@Transactional
public void A() { //여기에 문제가 생겨도 롤백 (B도 롤백)
  B();
}

@Transactional
public void B() { //여기에 문제가 생겨도 롤백 (A도 롤백))
}

내부 트랜잭션도 @Transactional이 붙어있으니까 작업 완료 시 commit()을 호출할텐데 그러면 DB에 이미 반영된게 아닐까?
정답은 아니다. 내부 트랜잭션은 외부 트랜잭션으로부터 전파됐기 때문에 새 트랜잭션을 실행하지 않았으므로 commit()을 호출해도
DB에 반영되는 로직이 수행되지 않는다. 새 트랜잭션을 실행한 외부 트랜잭션에서 호출해야 반영된다.

즉, 전체 트랜잭션이 commit되려면 내부/외부 트랜잭션에 모두 문제가 없어야 한다.

별도 트랜잭션으로 관리하기

트랜잭션 전파로 인해 내부/외부 트랜잭션이 한번에 롤백되기를 원치 않을 수 있다. 이 경우 2가지 방법으로 해결할 수 있다.

  • 내부/외부 트랜잭션 함수를 분리하기
  • @TransactionalREQUIRES_NEW 적용하기

일반적으로 함수를 분리하는 방법이 구분하기 쉽고 깔끔하다고 한다. 하지만 작성된 로직의 특성상 REQUIRES_NEW를 적용하는게
가장 나은 선택이 될 수도 있으므로 상황에 맞게 적용하는게 좋다. 참고로 @Transactional의 기본값은 REQUIRED이다.

트랜잭션 전파 시 주의할 점

위처럼 A, B 함수가 있는데, A는 트랜잭션이 적용되지 않았다고 하자. 아래 예시를 보자.

public void A() {
  B();
}

@Transactional
public void B() {
}

이때 B에는 트랜잭션이 적용될까?
정답은 아니다.

@Transactional이 붙은 함수는 스프링에서 호출할 때 프록시 패턴으로 동작한다.
그니까 트랜잭션 시작/종료 함수 호출 로직이 포함된 프록시 함수를 대신해서 호출한다. 정의된 함수를 그대로 호출하지 않는다.
개발자가 직접 정의하지 않은 프록시 함수를 호출할 수 있는 이유는 스프링이 프록시 함수를 스프링 빈으로 등록해서 실행 시점에 의존
관계를 주입해주기 때문이다!

A는 @Transactional을 적용하지 않아 스프링 빈으로 관리되지 않고, 직접 정의한 B를 호출한다.
즉, B의 프록시 함수를 호출하지 않는다. @Transactional을 B에 적용했지만 B의 프록시 함수가 아닌 직접 정의한 B를 호출하므로
실질적으로 트랜잭션 내에서 B 함수 로직이 수행되지 않는 것이다.

때문에 이런 일이 발생하지 않도록 B를 바로 호출하거나, A도 @Transactional을 적용해 트랜잭션을 전파시키면 된다.

References

  • 스프링 DB 2편 - 데이터 접근 활용 기술

카테고리:

업데이트:

댓글남기기