@Transactional의 동작 원리

이건우·2025년 3월 29일

웹 프로그래밍

목록 보기
37/43

@Transactional에 대해서 알기 위해, 먼저
DB의 Transaction에 대해서 먼저 짚고 넘어가겠습니다.

트랜잭션(Transaction)이란?

데이터베이스의 작업 단위(Unit of Work)로,
여러 작업이 하나의 논리적 단위로 묶여서 모두 성공하거나 모두 실패해야 하는 개념입니다.

비유하자면,
DB를 일종의 문서라고 했을 때에,
작업A 에서 문서의 a항목의 제목을 "감자" 에서 "고구마" 로 수정하고
b항목을 수정하려다 실패하여 작업이 끝났다면
작업B 에서 문서의 a항목을 확인하면 작업A에서 수정한 "고구마" 가 아니라,
"감자" 가 됩니다.

요약하자면, 각 작업의 내용이 전부 적용되거나, 전부 적용되지 않은
각 작업의 단위트랜잭션 이라고 보면 될 것 같습니다.

트랜잭션의 4가지 성질 (ACID)

트랜잭션은 DB에 적용 될 때에 4가지의 성질을 지니게 됩니다.
추가로 알아두면 좋을 것 같습니다.

항목설명
A - 원자성(Atomicity)모두 성공 or 모두 실패 (쪼갤 수 없음)
C - 일관성(Consistency)트랜잭션 전후 상태가 일관성 유지됨
I - 고립성(Isolation)동시에 실행되는 트랜잭션이 서로 간섭하지 않음
D - 지속성(Durability)커밋된 데이터는 영구 저장됨

그럼 이제, @Transactional에 대해서 알아보겠습니다.

@Transactional이란?

스프링에서 메서드 단위로 트랜잭션 처리를 쉽게 할 수 있게 해주는
어노테이션 기반 트랜잭션 처리 기능 으로,

아래처럼 쓰기만 하면 트랜잭션이 자동으로 관리됩니다:
예시로 적은 코드이므로 굳이 DB에서 다시 Email을 가져왔습니다.

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

위 코드에서는
userRepository.save(user);
userRepository.getUserEmailById(user.getUserId);
으로 유저 정보를 저장하고, userId로 userEmail을 조회합니다.

@Transactional어노테이션이 없었다면, 각 메소드마다 따로 트랜잭션이 적용되어
DB에 2번 연결되어야 하지만, DB요청 작업들을 하나로 묶어서 한번에 시행하여,
DB에 1번만 연결되면 되도록 해 주는 것입니다!

이는 각 시퀀스 수행시간을 유의미하게 단축시켜 주니,
한번쯤 실험해 보시는 것도 괜찮을 것 같습니다.

어떻게 동작하는가?

프록시 기반 AOP

@Transactional은 , Spring AOP 프록시 기반으로 동작하며,
Spring에서는 AOP를 이용해 트랜잭션, 로깅, 보안 같은 공통 기능을 메서드 실행 전후에 끼워넣는 방식으로 사용합니다.

요약하자면, Spring AOP 프록시는
우리가 만든 클래스는 그대로 두고, 가짜 객체(프록시)를 하나 감싸서
대신 동작하게 하는 방식으로 실행됩니다.

트랜잭션이 걸린 메서드가 호출되면,
1. 트랜잭션 시작
2. 메서드 실행
    3-1. 성공 시 커밋
    3-2. 예외 발생 시 롤백

!주의점!

this.메소드()와 같이 해당 객체 내부의 메소드를 호출하면,
해당 메소드는 Spring 프록시를 거치지 않고 JVM에서 직접 실행되기 때문에,
프록시를 우회하여 실행하게 되어 동일 트랜잭션에 포함되지 않게 됩니다.

이같은 경우에는
1. 해당 메서드를 외부 Bean으로 분리
2. ApplicationContext에서 자기 자신을 프록시로 꺼내 호출
ex)

    @Autowired
    private ApplicationContext applicationContext;

    public void outer() {
        MyService proxy = applicationContext.getBean(MyService.class);
        proxy.inner(); // ✅ 프록시를 통해 호출됨 → 트랜잭션 적용
    }

    @Transactional
    public void inner() {
        // 트랜잭션 적용됨
    }
  1. AOP 프레임워크 변경
  2. 현재 실행 중인 프록시를 얻어서 호출하여 실행
    ex)
@Service
public class MyService {

    public void outer() {
        // 현재 실행 중인 프록시를 얻어서 호출
        ((MyService) AopContext.currentProxy()).inner();
    }

    @Transactional
    public void inner() {
        System.out.println("트랜잭션 적용.");
    }
}
  1. 메소드 객체 외부로 이동
  2. 내부 호출을 외부처럼 만들기
    ex)
@Component
public class MyTxSupport {
    @Transactional
    public void txMethod() { ... }
}

@Service
@RequiredArgsConstructor
public class MyService {
    private final MyTxSupport myTxSupport;

    public void doSomething() {
        myTxSupport.txMethod(); // 프록시 호출됨
    }
}

이렇게 여러 가지 방법으로 프록시 우회를 극복할 수 있는 방안들이 있으며,
가장 간단한 방법으로는 6. 처럼 컴포넌트로 따로 분리하여 선언해서 사용하는
방식을 사용하는것이 가독성이나 구조적으로 안정적으로 보입니다 (개인의견)

한 줄 요약

@Transactional은 복잡한 트랜잭션 코드를 사용할 필요 없이,
AOP 기반으로 자동 커밋/롤백을 처리해주는 스프링의 기능입니다.
간단한 방법으로 트랜잭션 횟수를 줄여 줄 주 있는 강력한 기능임으로,
꼭 사용해 보시는 것을 추천합니다!

profile
새싹개발자

0개의 댓글