트랜잭션에 대해 이미 알고 있는 것
- 예외 발생 시 롤백해줌
- 스프링에서 주로 서비스 계층 클래스에 @Transactional 애노테이션을 붙여서 사용
- 클래스에 @Transactional 붙이면 메서드마다 적용됨
트랜잭션 스크립트 vs 선언적 트랜잭션
A, B, C라는 메서드가 있을 때 A, B, C를 하나의 트랜잭션에 넣으려면?
트랜잭션 스크립트
Connection connection = dataSource.getConnection();
try {
connection.setAutoCommit(false);
doSomething(connection, id, ...);
connection.commit();
} catch (Exception e) {
connection.rollback();
throw new IllegalStateException(e);
} finally {
release(connection);
}
- doSomething()과 같이 A-B-C를 묶어줘야 함
- connection이 반복하여 등장
- 트랜잭션 정보가 담긴 connection이 파라미터로 계속 따라다녀야 함
- 비즈니스 로직과 데이터 액세스 로직이 혼재
선언적 트랜잭션
- 위 트랜잭션 스크립트의 문제점 해결
- 트랜잭션의 시작/종료 시점을 별도로 설정
- 전파 속성이 있어 A에서 시작된 트랜잭션에 B, C가 자동으로 참여함
스프링의 트랜잭션 추상화
- 트랜잭션 서비스 많음
- 스프링은 트랜잭션 서비스와 데이터 액세스 기술의 종속을 방지하기 위해 추상화 적용
PlatformTransactionManager
- 트랜잭션 추상화의 핵심 인터페이스
- 메서드
- getTransaction(TransactionDefinition)
- commit(TransactionStatus)
- rollback(TransactionStatus)
- 이 인터페이스를 구현한 트랜잭션 서비스 클래스 사용
- DataSourceTransactionManager, JpaTransactionManager, JtaTransactionManager* ...
*JtaTransactionManager
여러 대 DB에 하나의 트랜잭션을 적용하거나 분산된 서버에서 진행되는 작업을 하나의 트랜잭션으로 만들 때 사용
하나의 DB에 하나의 트랜잭션 매니저를 사용해야 한다
- 만약 DB가 여러 개인데 하나의 트랜잭션을 만들고 싶다면 JtaTransactionManager를 하나 사용한다
- 만약 DB는 다르지만 테이블의 구조가 전부 같다면?
- 쉬운 예로 운영DB와 개발DB
- DAO 클래스 하나(MemberDao)로 빈과 트랜잭션 매니저는 각각 설정한다(memberDao1/memberDao2, transactionManager1/transactionManager2)
<bean id="dataSource1" class="...">...</bean>
<bean id="dataSource2" class="...">...</bean>
<bean id="memberDao1" class="...MemberDao">
<property name="dataSource" ref="dataSource1"/>
</bean>
<bean id="memberDao2" class="...MemberDao">
<property name="dataSource" ref="dataSource2"/>
</bean>
<bean id="transactionManager1" class="...DataSourceTransactionManager">
<property name="dataSource" ref="dataSource1"/>
</bean>
<bean id="transactionManager2" class="...DataSourceTransactionManager">
<property name="dataSource" ref="dataSource2"/>
</bean>
트랜잭션 경계 설정
트랜잭션의 시작과 종료과 되는 경계는 보통 서비스 계층의 메서드(비즈니스 로직)
코드로 설정
- PlatformTransactionManager의 메서드를 직접 사용
- 하지만 위 방법은 try/catch를 써야하기 때문에 TransactionTemplete 사용
- 실제 개발에서 자주 사용되진 않지만 테스트 시 필요할 수 있음
선언적 트랜잭션 경계 설정
- 트랜잭션은 대부분 성격이 비슷하기 때문에 대상에 일괄 적용하는 게 편함
- 경계 설정이라는 부가 기능을 AOP를 통해 빈에 적용
- tx 또는 @Transactional 사용
- tx
- 어떤 부가 기능을 사용할지 = 어드바이스
- 적용 대상 선정 = 포인트컷
- 트랜잭션 어드바이스 + 포인트컷 = 하나의 AOP 모듈 = 트랜잭션 어드바이저
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut" />
<aop:pointcut id="txPointcut" expression="execution(* *..MemberDao.*(..))" />
</aop:config>
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
트랜잭션 동작
프록시
- JDK 다이내믹 프록시 & CGLib
- 프록시란 대상 클래스(진짜 사용하려는 클래스) 대신에 클라이언트에게 주입되고
클라이언트가 메서드를 호출하면 대신 호출 당하여 부가 기능을 추가하고 대상 클래스에게 위임하는 객체
- 스프링의 AOP는 기본적으로 JDK 다이내믹 프록시 기법을 이용
- (스프링 부트는 기본적으로 CGLib 라이브러리를 사용)
- 동작 과정
- 메서드를 호출하면 프록시가 호출된다
- 트랜잭션 어드바이저(트랜잭션 AOP 모듈)가 부가 기능인 커밋/롤백 처리를 한다
- (커스텀 어드바이저가 있으면 트랜잭션 어드바이저 전후로 실행됨)
- 대상 메서드 호출
JDK 다이내믹 프록시
- 대상 클래스가 구현한 인터페이스를 구현(implements)하여 프록시 생성
- 그렇다면 반드시 인터페이스가 있어야 한다?
- 아니다 클래스만으로도 프록시 사용이 가능하다
CGLib
- 대상 클래스를 상속(extends) 받아 프록시 생성
- 런타임 바이트코드 조작
- 상속을 사용하므로 final 클래스에는 적용이 불가
AspectJ
- AOP 프레임워크
- 프록시와 달리 컴파일된 대상 클래스 파일을 수정하거나
클래스 로딩 시 바이트코드를 직접 조작하여 부가 기능 추가 - 세밀한 부가 기능 추가 가능
- 대상 클래스가 한 메서드에서 자기 자신의 다른 메서드를 호출했을 때도 부가 기능 적용
- @Transactional을 클래스와 클래스 메서드에 부여해야 함
@Transactional
- 스프링에서 익숙하게 사용하는 트랜잭셔널 애노테이션
- 설정
- <tx:annotaion-driven /> 추가하여 사용
- @Configuration 클래스에 @EnableTransactionManagement 애노테이션 추가
- 스프링 부트 사용 시 spring-data-* 또는 spring-tx 의존성 설정하면 PlatformTransactionManager가 자동 추가됨
- 메서드, 클래스, 인터페이스에 붙인다
- 적용 우선 순위
- 클래스의 메서드
- 클래스
- 인터페이스의 메서드
- 인터페이스
트랜잭션 속성
트랜잭션 전파
트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법을 결정하는 속성
- REQUIRED
- 디폴트 속성
- 트랜잭션이 시작되고 다른 메서드 호출 시 같은 트랜잭션으로 참여됨
- REQUERES_NEW
- 항상 새로운 트랜잭션 진행
- 이미 진행 중인 트랜잭션 있으면 그 트랜잭션 보류
- NESTED
- 이미 진행 중인 트랜잭션이 있으면 중첩 트랜잭션 시작
- 부모 트랜잭션의 커밋과 롤백에는 영향을 받으나 자신의 커밋과 롤백은 부모에게 영향을 주지 않음
- ex) 중요한 작업 중 로그를 DB에 저장해야 하는 상황
- 로그가 실패해서 롤백하더라도 중요한 작업은 롤백하면 안 됨
- 중요한 작업이 실패하면 로그도 같이 롤백해야 함
그 외 SUPPORTS, MANDATORY, NOT_SUPPORTED, NEVER 등의 전파 속성이 있다.
트랜잭션 격리
격리 수준 | 설명 |
---|---|
DEFAULT | 사용하는 데이터 액세스 기술이나 DB 드라이버의 디폴트 설정을 따름 대부분은 READ_COMMITTED이나 설정을 확인하는 것이 좋음 |
READ_UNCOMMITTED | 가장 낮은 격리 수준 하나의 트랜잭션이 커밋되기 전 변화가 다른 트랜잭션에게 노출 가장 빠르므로 성능은 좋으나 데이터의 일관성이 떨어짐 |
READ_COMMITTED | 가장 많이 사용되는 격리 수준 트랜잭션이 커밋하지 않은 정보는 다른 트랜잭션에게 노출되지 않음 |
REPEATABLE_READ | 하나의 트랜잭션이 읽은 정보(로우)를 다른 트랜잭션이 수정하는 것을 막아줌 |
SERIALIZABLE | 가장 강력한 격리 수준 이름 그대로 트랜잭션을 순차적으로 진행 여러 트랜잭션이 동시에 같은 테이블의 정보를 액세스하지 못 함 안전하지만 성능은 떨어짐 |
읽기전용 read-only
- 읽기 전용 메서드에 read only 속성을 부여하면 성능이 향상됨
- 읽기 전용 트랜잭션이 시작되고 insert, update, delete 같은 쓰기 작업이 진행되면 예외 발생
- tx 경우 getXXX, findXXX 같이 조회성 메서드 이름에 패턴을 부여하고 패턴에 읽기 전용 일괄 적용
- @Transactional은 하나하나 부여
트랜잭션 롤백 예외 rollback-for
- 기본적으로 런타임 예외 시 롤백 (unchecked exception)
- 체크 예외(checked exception)가 롤백 대상이 아닌 커밋 대상이 되는 이유는 보통 비즈니스적인 의미가 있는 결과를 돌려주는 용도로 사용되기 때문
- 물론 체크 예외 롤백도 설정 가능
그 외 트랜잭션 제한시간, 트랜잭션 커밋 예외 등의 트랜잭션 설정이 있다.
트랜잭션에 대해 이미 알고 있는 것에 더해보기
- 예외 발생 시 롤백해줌
- 기본적으로 런타임 예외(unchecked exception)가 발생하면 롤백
- 스프링에서 주로 서비스 계층 클래스에 @Transactional 애노테이션을 붙여서 사용
- 메서드, 클래스, 인터페이스에 붙일 수 있다는데 그럼 어디에 붙여야 하나?
- 상황에 알맞게 붙이면 된다
- 다만 스프링은 구현된 클래스와 그 메서드에 붙이길 권장하고 있다
- 인터페이스에 붙일 경우 인터페이스 기반 프록시에만 적용되기 때문
- 클래스에 @Transactional 붙이면 메서드마다 적용됨
- 문제 상황: 프록시 사용 시 메서드에서 같은 클래스의 다른 메서드 호출 시 트랜잭션 적용 안 됨
- 해결: 다른 클래스로 분리하기, AspectJ 사용
참고
<토비의 스프링 3.1>
'Study > Spring' 카테고리의 다른 글
Spring과 Spring Boot 차이 한 줄 요약 (0) | 2023.03.10 |
---|---|
Spring Service단에 Interface가 필요한가? (0) | 2022.05.06 |