스프링 @Transactional

2025. 12. 29. 16:22Web Development

1. @Transactional

: 메서드(또는 클래스) 경계에서 트랜잭션을 시작/종료하도록 스프링이 개입하게 만드는 애노테이션

• 메서드 진입 시점: 트랜잭션 시작(필요 시 생성, 또는 기존 트랜잭션 참여)
• 정상 종료 시점: 커밋
• 예외 발생 시점: 롤백(롤백 규칙에 해당하면)

(point ⭐️)
개발자가 Connection.setAutoCommit(false), commit(), rollback() 같은 코드를 직접 쓰지 않아도 되도록 트랜잭션 경계 설정을 선언적으로 제공함



2. 프록시의 등장이유(@Transactional = AOP 기반)


스프링의 선언적 트랜잭션은 기본적으로 AOP(Aspect Oriented Programming) 로 구현된다.
즉, 실제 비즈니스 메서드 호출을 “가로채서” 트랜잭션 처리를 앞뒤로 감싸야 한다.
이를 위해 스프링은 빈을 등록할 때 다음 중 하나 방식으로 프록시 객체를 만들어 빈 대신 등록한다.

• JDK 동적 프록시: 인터페이스 기반 프록시
• CGLIB 프록시: 클래스 상속 기반 프록시(인터페이스 없어도 가능)

결론적으로, 컨테이너에서 주입받는 객체는 “원본 빈”이 아니라 “프록시 빈”인 경우가 많고, 그 프록시가 메서드 호출 전후에 트랜잭션을 제어한다.


3. 내부 구성 컴포넌트


3.1 트랜잭션 AOP의 대표 구성 요소
TransactionInterceptor
실제로 “메서드 호출을 감싸며” 트랜잭션 시작/커밋/롤백을 수행하는 인터셉터(어드바이스)
PlatformTransactionManager
트랜잭션을 실제로 시작/종료하는 전략 인터페이스(트랜잭션 매니저)
예: DataSourceTransactionManager(JDBC), JpaTransactionManager(JPA)
TransactionAttribute / TransactionAttributeSource
@Transactional에 적힌 전파/격리/읽기전용/타임아웃/롤백 규칙 등을 해석해 “정책”으로 만든 것

3.2 호출 흐름
1. 클라이언트(다른 빈)가 서비스 메서드를 호출
2. 실제로는 “서비스 프록시”의 메서드가 호출됨
3. 프록시가 TransactionInterceptor로 위임
4. 인터셉터가 @Transactional 메타데이터를 분석
5. PlatformTransactionManager로 트랜잭션 시작(또는 참여)
6. 원본 타깃 메서드 실행
7. 정상 종료면 commit, 예외면 롤백 규칙에 따라 rollback


4. @Transactional 사용 위치 (클래스 vs 메서드)


4.1 메서드
메서드에 붙이면 그 메서드에만 정책이 적용된다.

4.2 클래스
클래스에 붙이면 기본 정책이 “클래스의 모든 public 메서드”에 적용되고, 메서드에 붙인 것이 우선한다(덮어씀).

실무에선 보통
• 서비스 클래스에 기본 @Transactional(readOnly = true)
• 쓰기 메서드(등록/수정/삭제)에만 @Transactional(readOnly=false)로 오버라이드



5. 프록시 기반 트랜잭션에서 가장 중요한 제약

: “프록시를 거쳐야 적용된다”

여기서 많은 장애가 발생할 수도 있다.

5.1 Self-invocation(자기 자신 호출) 문제

같은 클래스 내부에서 this.someTxMethod()처럼 호출하면 프록시를 거치지 않고 원본 메서드를 직접 호출하게 된다. 그러면 @Transactional이 붙어 있어도 트랜잭션이 시작되지 않습니다.

대표사례

@Service
public class OrderService {
  public void outer() {
    inner(); // 같은 클래스 내부 호출 -> 프록시 미경유 -> @Transactional 적용 안 됨
  }

  @Transactional
  public void inner() { ... }
}


회피 패턴(대표 3가지)
• 트랜잭션 경계를 외부 빈(다른 서비스)로 분리
• 프록시를 주입받아 프록시를 통해 호출(권장도는 상황별)
• AspectJ 방식(바이트코드 위빙) 사용(운영/구성 난이도 상승)


5.2 기본적으로 “public 메서드”에 적용되는 경우가 많다

프록시 기반 AOP는 일반적으로 외부에서 호출 가능한 메서드(대개 public)를 기준으로 적용된다.
private/protected에 붙였는데 안 먹는다고 느끼는 경우가 흔하다.


6. 롤백 규칙

: “RuntimeException은 롤백, Checked Exception은 기본 커밋”

스프링의 기본 룰⭐️⭐️⭐️
• Unchecked 예외(RuntimeException, Error) → 롤백
• Checked 예외(Exception 하위의 체크 예외) → 기본적으로 커밋(롤백 안 함)

따라서 체크 예외에서 롤백하고 싶으면 옵션을 명시해야함!

@Transactional(rollbackFor = Exception.class)
public void doWork() throws Exception { ... }


반대로 특정 런타임 예외는 롤백하지 않게 하고 싶다면,

@Transactional(noRollbackFor = IllegalStateException.class)
public void doWork() { ... }


7. 전파(Propagation)

: “기존 트랜잭션이 있으면 어떻게 할 것인가”

전파는 “이미 트랜잭션이 존재할 때”의 행동 규칙이다.
REQUIRED(기본값)
있으면 참여, 없으면 새로 생성
실무 대부분은 이 기본값으로 해결된다.
• REQUIRES_NEW
무조건 새 트랜잭션 시작(기존 트랜잭션은 잠시 보류)
“메인 로직이 실패해도 로그/이력은 반드시 남긴다” 같은 요구에서 사용
단, DB 커넥션을 추가로 점유할 수 있어 부담이 생길 수 있다.
• NESTED
저장점(Savepoint) 기반 중첩 트랜잭션 (DB/트랜잭션 매니저 지원 조건 있음)
• SUPPORTS / NOT_SUPPORTED / NEVER / MANDATORY
특수 상황에서 사용(트랜잭션 강제/금지/존재 요구 등)



8. 격리수준(Isolation)

: “동시성에서 어떤 현상을 허용할 것인가”

격리수준은 DB 트랜잭션 격리 개념과 직결됩니다.
• READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE 등
• 실무에서는 보통 DB 기본 격리수준을 따르고, 정말 필요한 케이스에서만 조정합니다.
• 무리한 격리수준 상향은 락 경합/성능 문제를 만들 수 있습니다.


9. readOnly = true


@Transactional(readOnly = true)는 “나는 쓰기 안 할 거야”라는 힌트이다.
• JDBC 레벨에서 read-only 힌트를 줄 수 있고,
• JPA(Hibernate)에서는 flush 동작이나 변경 감지 비용을 줄이는 최적화에 영향을 줄 수 있다.

다만 “무조건 쓰기 차단”을 보장하는 절대 규칙이라기보다는, 구현체/드라이버/환경에 따라 최적화 힌트 성격이 강하다. (그래도 서비스 조회 계층에 readOnly를 거는 패턴은 매우 보편적임.)


10. 실무에서 자주 겪는 트랜잭션 관련 에러사항

1. @Transactional 붙였는데 트랜잭션이 안 걸린다
• 내부호출(self-invocation)인가?
• 프록시를 거치고 있는가?
• 빈으로 등록된 객체를 호출하고 있는가(직접 new로 생성한 객체면 당연히 안 됨)?
• 메서드 접근제어자/프록시 방식(JDK vs CGLIB) 이슈는 없는가?
2. 예외가 났는데 롤백이 안 된다
• 체크 예외(checked exception)라 기본 커밋된 건 아닌가?
• try/catch로 예외를 삼켜서 정상 종료처럼 보이게 만든 건 아닌가?
• catch 후 롤백 원하면 다시 던지거나 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 같은 처리가 필요
3. 트랜잭션이 너무 넓다
• 외부 API 호출, 파일 IO, 대기/슬립 등 DB와 무관한 작업을 트랜잭션 안에 넣으면 락/커넥션 점유가 길어져 장애로 이어질 수 있다.




실제 회사 프로젝트에서 사용하고 있는 방식을 알아보자
- 전자정부프레임워크 eGovE 기반(spring 4.x.x)


1) dispatcher-servlet.xml

: 주로 웹(MVC) 컨텍스트 설정 + (컨트롤러/웹단에 걸리는) AOP 설정


1. <aop:aspectj-autoproxy proxy-target-class="true" />
• 스프링 AOP 프록시 자동 생성 기능을 켠다는 의미입니다.
• proxy-target-class="true" 때문에 CGLIB(클래스 기반 프록시) 를 강제합니다.
• 인터페이스가 없어도 프록시 생성 가능
• 대신 final 클래스/메서드에는 제약이 생길 수 있음

즉, 이 설정은 “@Transactional” 자체라기보다는,
스프링이 AOP 프록시를 만들 수 있는 기반을 켜는 설정임

2. <bean id="controllerAop" ... />, <bean id="serviceAop" ... />
• ControllerAop, ServiceAop 클래스가 실제로 AOP로 동작하려면 보통:
• 클래스에 @Aspect + @Around/@Before/... 등이 있거나,
• 또는 XML <aop:config>로 pointcut/advice를 묶어주는 구성이 있어야 합니다.
• 즉, 이 빈들은 대개 “로깅/권한/공통처리” 같은 커스텀 AOP 목적일 가능성이 큽니다.

3. mvc:annotation-driven, task:annotation-driven
• @Controller, @RequestMapping, 메시지컨버터 등의 MVC 기능
• @Scheduled, @Async 같은 태스크 기능
트랜잭션과는 직접 관계는 없고, 웹/스케줄링 기반 기능 활성화

• dispatcher-servlet.xml은 “AOP 프록시를 쓰는 환경”을 켜고, 컨트롤러/서비스에 대한 커스텀 AOP(로깅 등)를 적용하기 위한 설정이며,
• 트랜잭션을 실제로 켜는 핵심은 context-transaction.xml 이다.


2) context-transaction.xml

: 트랜잭션 인프라와 “서비스 메서드에 트랜잭션을 어떻게 적용할지” 정책 설정

“트랜잭션을 애노테이션 기반 + 포인트컷 기반”으로 둘 다 켜놓은 형태입니다.

1. 트랜잭션 매니저 등록

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
</bean>

• JDBC(MyBatis/iBATIS 포함) 계열에서 흔히 쓰는 트랜잭션 매니저
• dataSource로 커넥션을 얻어서
• begin/commit/rollback을 수행합니다.
• 즉, “트랜잭션을 실제로 시작/종료하는 엔진”입니다.

2. @Transactional을 활성화

<tx:annotation-driven transaction-manager="txManager" />

•  @Transactional을 해석하고 적용하는 인프라를 등록합니다.
• 결과적으로, @Transactional이 붙은 메서드는 프록시를 통해 호출될 때 트랜잭션이 걸립니다.


3. “애노테이션 없이도” 서비스에 트랜잭션을 강제로 적용하는 규칙 정의

<tx:advice id="txAdvice" transaction-manager="txManager">
  <tx:attributes>
    <tx:method name="*" propagation="REQUIRED" rollback-for="Exception" />
  </tx:attributes>
</tx:advice>

여기서 중요한 포인트:
• name="*" : 매칭된 메서드는 전부 적용
• propagation="REQUIRED" : 트랜잭션 있으면 참여, 없으면 새로 시작(기본값)
• rollback-for="Exception" : Checked Exception까지 포함해서 롤백하도록 확장
• 기본 스프링 규칙은 RuntimeException만 롤백인데,
• 이 설정은 Exception이면 롤백으로 더 강하게 걸어둔 상태이다.

즉, 이 블록은 “서비스 메서드는 기본적으로 전부 트랜잭션 걸고, Exception이면 다 롤백”이라는 정책이다.

4. 어떤 메서드에 적용할지: 포인트컷 + 어드바이저

<aop:config>
  <aop:pointcut id="requiredTx"
      expression="execution(* mofa.aad..*Service*.*(..))"/>
  <aop:advisor advice-ref="txAdvice" pointcut-ref="requiredTx"/>
</aop:config>

• execution(* mofa.aad..*Service*.*(..))
• mofa.aad 패키지 하위
• 클래스명이 *Service*로 매칭되는 타입의
• 모든 메서드에 적용
• 그 메서드들에 txAdvice를 붙인다(=트랜잭션 인터셉터 적용)


mofa.aad.. 아래의 *Service* 클래스의 모든 메서드는
@Transactional을 안 붙여도 트랜잭션(REQUIRED) + Exception 롤백이 적용된다.


3) 두 설정이 같이 있을 때의 의미⭐️

사실상 트랜잭션 적용 방식이 2개가 공존하고 있다.
1. @Transactional 기반 (tx:annotation-driven)
2. 포인트컷 기반(서비스는 전부 txAdvice 적용)


• 서비스 메서드가 포인트컷에 걸리면 애노테이션 없어도 트랜잭션이 걸린다
• 서비스 메서드에 @Transactional까지 붙어 있으면
• 두 Advisor가 동시에 적용될 수도 있는데,
• 대부분 REQUIRED라서 내부적으로는 “기존 트랜잭션 참여” 형태가 되어 동작은 큰 문제 없이 돌아가는 경우가 많습니다.
• 다만 “롤백 규칙/우선순위/가독성” 측면에서 혼란을 만들 수 있다.
• 예: 애노테이션은 기본 롤백(런타임만)인데, XML advice는 Exception 롤백이면 → 실제 롤백이 더 강하게 적용될 수 있음

❗️ 이 구성이 자주 사용될까?
• 레거시(eGovFrame/구형 Spring XML)에서는 꽤 흔한 방식
• “서비스는 기본으로 다 트랜잭션”이라는 운영 정책을 XML로 박아두고,
• 일부 예외 케이스만 애노테이션으로 튜닝





”스프링부트- 데이터소스 자동등록“


- 스프링부트는 데이터소스(dataSource)를 스프링 빈에 자동으로 등록한다
- 자동으로 등록되는 스프링 빈 이름 : dataSource
- 참고: 개발자가 직접 데이터소스를 빈으로 등록하면, 스프링 부트는 데이터소스를 자동으로 등록하지 않는다.

스프링 부트는 ‘application.properties’ 에 있는 속성을 사용해 데이터소스를 생성하고, 빈에 등록한다.


”스프링부트- 트랜잭션 매니저 자동등록“
- 스프링부트는 적절한 트랜잭션 매니저(PlatformTransactionManager)를 자동으로 스프링 빈에 등록한다.
- 자동으로 등록되는 빈의 이름: transactionManager
- 참고: 개발자가 직접 트랜잭션 매니저를 빈으로 등록하면, 스프링 부트는 트랜잭션 매니저를 자동으로 등록하지 않는다.

📌 어떤 트랜잭션 매니저를 등록할까?
현재 등록된 라이브러리를 보고 판단.
ex) JDBC 기술을 사용중 -> DataSourceTransactionManager
JPA를 사용중-> JpaTransactionManager
둘다 사용할 경우 -> JpaTransactionManager
(JpaTransactionManager는 DataSourceTransactionManager가 제공하는 기능 대부분을 지원)

'Web Development' 카테고리의 다른 글

스프링 데이터 접근 예외 추상화 정리  (1) 2026.01.09
자바 예외처리  (0) 2025.12.31
스프링 트랜잭션(Transaction)  (1) 2025.12.23
커넥션 풀  (0) 2025.12.18
JDBC 개념 정리  (0) 2025.12.16