[Spring] @Transactional

2026. 5. 23. 22:00·Framework/Spring

 

안녕하세요.

오늘은 Spring의 annotation 중에 @Transactional이 무엇이며 내부적으로 어떻게 동작하는지를 정리해보려고 합니다.

사용법은 단순해 보이지만 동작 방식을 모르고 쓰면 데이터 정합성 사고로 이어지기 쉬운 어노테이션입니다.

이 글에서는 트랜잭션의 기본 개념부터 @Transactional이 내부적으로 어떤 방식으로 동작하는지까지 차근차근 살펴보겠습니다.

 

 

Transaction이란?

Transaction은 하나의 논리적 작업 단위로 묶인 데이터베이스 연산들의 집합입니다.

핵심은 "전부 성공하거나, 전부 실패한다"는 점입니다. 가장 자주 인용되는 예시는 계좌 이체입니다.

1. A 계좌에서 10만원 출금
2. B 계좌에 10만원 입금

1번이 성공한 뒤 2번이 실패하면 A의 돈만 사라지고 B는 받지 못합니다.

트랜잭션은 이런 상황을 막기 위해 두 작업을 한 단위로 묶어, 2번이 실패하면 1번도 되돌립니다(롤백).

데이터베이스 트랜잭션은 다음 네 가지 속성(ACID)을 보장합니다.

속성 의미
Atomicity (원자성) 모두 성공하거나 모두 실패
Consistency (일관성) 트랜잭션 전후 데이터 무결성 유지
Isolation (격리성) 동시 실행 트랜잭션 간 간섭 방지
Durability (지속성) 커밋된 결과는 영속적으로 저장

@Transactional은 이 보장을 코드에서 손쉽게 활용할 수 있게 해주는 도구입니다.

 

 

Spring 이전의 Transaction 관리

@Transactional이 없을 때 트랜잭션을 어떻게 관리했는지 보면, 이 어노테이션의 가치가 더 명확해집니다.

public void transfer(Long fromId, Long toId, long amount) {
    Connection conn = null;
    try {
        conn = dataSource.getConnection();
        conn.setAutoCommit(false);  // 트랜잭션 시작

        accountDao.withdraw(conn, fromId, amount);
        accountDao.deposit(conn, toId, amount);

        conn.commit();  // 커밋
    } catch (Exception e) {
        if (conn != null) {
            try {
                conn.rollback();  // 롤백
            } catch (SQLException ex) {
                log.error("롤백 실패", ex);
            }
        }
        throw new RuntimeException(e);
    } finally {
        if (conn != null) {
            try {
                conn.close();  // 리소스 정리
            } catch (SQLException ex) {
                log.error("커넥션 종료 실패", ex);
            }
        }
    }
}

 

위의 코드를 보면 문제점이 명확합니다.

  • 비즈니스 로직과 트랜잭션 코드의 혼재 : 실제 비즈니스 로직은 withdraw와 deposit 두 줄뿐
  • 반복적인 보일러플레이트 : 메서드마다 같은 try-catch-finally 구조
  • 휴먼 에러 가능성 : commit() 누락, rollback() 누락, close() 누락
  • 트랜잭션 전파의 어려움 : 다른 메서드와 같은 트랜잭션을 유지하려면 Connection을 직접 들고 다녀야 함

같은 코드를 메서드마다 반복하는 것은 유지보수 관점에서도 좋지 않습니다.

 

 

@Transactional이란?

@Transactional은 Spring에서 트랜잭션을 선언적으로 관리할 수 있게 해주는 어노테이션입니다.

메서드(또는 클래스)에 붙이기만 하면 해당 메서드를 실행하는 동안 자동으로 트랜잭션이 시작되고, 정상 종료 시 커밋, 예외 발생 시 롤백됩니다.

앞서 살펴본 절차적 트랜잭션 관리 코드는 다음과 같이 단순해집니다.

@Service
@RequiredArgsConstructor
public class AccountService {

    private final AccountRepository accountRepository;

    @Transactional
    public void transfer(Long fromId, Long toId, long amount) {
        accountRepository.withdraw(fromId, amount);
        accountRepository.deposit(toId, amount);
    }
}

어노테이션 한 줄로 다음이 모두 자동 처리됩니다.

  • 메서드 시작 시 트랜잭션 시작 (setAutoCommit(false))
  • 정상 종료 시 커밋
  • 예외 발생 시 롤백
  • 메서드 종료 시 커넥션 반환

비즈니스 로직만 남고, 트랜잭션 관리 코드는 사라집니다. 이를 선언적 트랜잭션 관리(Declarative Transaction Management)라고 부릅니다.

 

 

@Transactional 선언 위치

위치 동작
메서드 레벨 특정 메서드에만 적용 (가장 일반적)
클래스 레벨 클래스 내 모든 public 메서드에 일괄 적용
인터페이스 레벨 JDK 프록시일 때만 동작, CGLIB(Spring Boot 2.x 기본)에서는 무시됨. 구현체에 붙여야 함.

 

우선순위: 메서드 레벨 > 클래스 레벨

@Service
@Transactional(readOnly = true)  // 클래스 레벨: 기본은 readOnly
public class UserService {

    public UserDto findUser(Long id) { ... }  // readOnly = true 상속

    @Transactional  // 메서드 레벨: 클래스 레벨을 override
    public void register(UserDto dto) { ... }  // 일반 트랜잭션
}

조회 서비스에서 자주 쓰는 패턴입니다. 클래스 전체를 readOnly로 두고, 쓰기 메서드만 별도로 표시합니다.

 

 

Spring AOP 프록시

@Transactional의 진짜 핵심은 Spring AOP 프록시에 있습니다.

어노테이션을 붙이기만 했는데 트랜잭션이 자동으로 처리되는 동작은 사실 스프링이 우리 빈을 프록시로 감싸서 가능한 것입니다.

 

프록시 객체란?

스프링 컨테이너는 @Transactional이 붙은 빈을 그대로 등록하지 않고, 프록시 객체로 감싸서 등록합니다.

구분 설명
실제 객체 AccountService 인스턴스
프록시 객체 AccountService를 감싼 트랜잭션 처리용 객체
빈으로 등록되는 것 프록시 객체

외부에서 빈을 호출하면 항상 프록시가 먼저 호출되고, 프록시는 트랜잭션 처리 후 실제 객체로 위임합니다.

[Caller] → [프록시] → 트랜잭션 시작 → [실제 Bean] → 결과 → [프록시] → 커밋/롤백

 

프록시 생성 방식

방식 조건 비고
JDK Dynamic Proxy 인터페이스 구현 시 인터페이스 기반 호출
CGLIB 인터페이스 없을 시 클래스 상속 기반

Spring Boot 2.x부터는 인터페이스 유무와 관계없이 CGLIB를 기본으로 사용합니다.

이 프록시 기반 동작 방식이 @Transactional의 가장 중요한 특성입니다.

@Transactional을 붙였는데도 트랜잭션이 동작하지 않는 대부분의 경우는, 호출이 프록시를 거치지 않았기 때문입니다.

 

 

기본 롤백 정책

@Transactional의 기본 롤백 정책은 다음과 같습니다.

예외 종류 롤백 여부
RuntimeException 및 하위 ✅ 롤백
Error 및 하위 ✅ 롤백
Exception 및 하위 (체크 예외) ❌ 롤백 안 됨

체크 예외가 롤백되지 않는 이유는, Spring이 체크 예외를 "복구 가능한 비즈니스 예외"로 간주하기 때문입니다.

 

롤백 정책 변경

@Transactional(rollbackFor = Exception.class)  // 모든 예외에 롤백
public void register(UserDto dto) throws IOException { ... }

@Transactional(noRollbackFor = MailSendException.class)  // 특정 예외만 제외
public void notify(...) { ... }

체크 예외도 롤백되기를 원한다면 rollbackFor를 명시해야 합니다.

 

 

@Transactional의 특징

특징 설명
선언적 관리 어노테이션 한 줄로 트랜잭션을 시작/커밋/롤백
AOP 프록시 기반 내부적으로 Spring AOP 프록시가 호출을 가로채 트랜잭션 처리
다양한 적용 위치 메서드, 클래스, 인터페이스 레벨 모두 적용 가능
세밀한 제어 옵션 propagation, isolation, readOnly, timeout, rollbackFor 등 다양한 속성 지원
기본 롤백 정책 존재 RuntimeException과 Error만 자동 롤백, 체크 예외는 별도 지정 필요

선언적이라는 점과 AOP 프록시로 동작한다는 점이 가장 중요한 특징입니다.

 

 

@Transactional의 장점

장점 설명
코드 분리 비즈니스 로직과 트랜잭션 코드를 깔끔하게 분리
보일러플레이트 제거 try-catch-finally, commit/rollback 코드가 사라짐
휴먼 에러 감소 commit(), rollback(), close() 누락 가능성 차단
트랜잭션 전파 자동 처리 Connection을 메서드 간에 직접 들고 다니지 않아도 됨
세밀한 제어 readOnly, isolation, timeout 등으로 상황에 맞게 조정 가능

비즈니스 로직만 보이도록 만들어준다는 점이 가장 큰 장점입니다.

 

 

@Transactional의 단점

쉬워 보이는 만큼 주의할 점도 많습니다.

단점 설명
프록시 미경유 시 무시됨 self-invocation, private/final 메서드 등에서는 트랜잭션이 적용되지 않음
직관과 다른 롤백 정책 체크 예외는 기본적으로 롤백되지 않음
적용 범위 파악의 어려움 클래스/메서드 레벨 우선순위, 전파 옵션이 얽혀 코드만 보고 동작 예측이 어려움
커넥션 점유 문제 트랜잭션 안에 외부 API 호출이나 긴 작업을 두면 DB 커넥션 풀 고갈 위험
디버깅의 복잡함 호출 스택에 프록시·AOP 인터셉터가 끼어들어 추적이 까다로움

여기서 말하는 "단점"은 어노테이션의 결함이라기보다, 사용법이 쉬운 만큼 모르고 쓰면 의도와 다르게 동작하기 쉬운 특성에 가깝습니다.

 

 

마무리 정리

이번 글에서는 @Transactional의 개념과 동작 원리를 정리했습니다. 핵심을 다시 짚어보면 다음과 같습니다.

  • 트랜잭션은 하나의 논리적 작업 단위이며 ACID를 보장한다.
  • @Transactional은 트랜잭션 관리를 선언적으로 처리해 비즈니스 로직과 분리해준다.
  • 내부적으로 Spring AOP 프록시가 메서드 호출을 감싸 트랜잭션을 시작·커밋·롤백한다.
  • 기본 롤백 정책은 RuntimeException과 Error만 해당된다.

@Transactional은 사용법이 단순한 만큼 함정도 많은 어노테이션입니다.

그 함정들의 대부분은 위에서 살펴본 프록시 기반 동작 방식과 기본 롤백 정책 두 가지에서 비롯됩니다.

이를 염두에 두면 실무에서 마주치는 대부분의 트랜잭션 이슈를 빠르게 진단할 수 있습니다.

 

읽어주셔서 감사합니다.

 

 

참조

  • https://docs.spring.io/spring-framework/docs/5.3.x/reference/html/data-access.html#transaction-declarative
  • https://docs.spring.io/spring-framework/docs/5.3.x/reference/html/data-access.html#tx-decl-explained
  • https://docs.spring.io/spring-framework/docs/5.3.x/reference/html/core.html#aop-understanding-aop-proxies
저작자표시 비영리 변경금지 (새창열림)

'Framework > Spring' 카테고리의 다른 글

[Spring] Spring Security 인증 처리  (0) 2026.05.31
[Spring] Spring Security  (0) 2026.05.30
[Spring] @Transactional의 8가지 함정  (0) 2026.05.24
'Framework/Spring' 카테고리의 다른 글
  • [Spring] Spring Security 인증 처리
  • [Spring] Spring Security
  • [Spring] @Transactional의 8가지 함정
으노로
으노로
  • 으노로
    study-library
    으노로
  • 전체
    오늘
    어제
    • 분류 전체보기 (42) N
      • Language (16)
        • JAVA (15)
        • JavaScript (1)
      • Framework (4) N
        • Spring (4) N
      • Web (4)
      • Infra (6)
      • Algorithm (10)
        • Programmers (10)
      • Database (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    문자열 정렬하기
    코딩테스트
    프로그래머스
    @transactional
    eclipse
    OS
    알고리즘
    분수의덧셈
    transactional
    문자열정렬하기(2)
    spring
    inmemorydb
    트렌잭션
    비동기 통신 방식
    programmers
    java
    스프링부트
    자바
    spring boot
    스프링
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
으노로
[Spring] @Transactional
상단으로

티스토리툴바