참고도서
|
1. 트랜잭션(Transaction)?
2. @Transactional
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost/spring4fs?characterEncoding=utf8" /> <property name="user" value="spring4" /> <property name="password" value="spring4" /> <property name="maxPoolSize" value="100" /> <property name="maxIdleTime" value="600" /> <property name="idleConnectionTestPeriod" value="300" /> </bean> <bean id="memberDao" class="spring.MemberDao"> <constructor-arg ref="dataSource" /> <!-- setter 메소드를 통해 JdbcTemplate 객체를 생성할 때 --> <!-- <property name="dataSource" ref="dataSource" /> --> </bean> <!-- DataSource 객체를 프로퍼티 값으로 받아서 PlatformTransactionManger 빈 객체 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!-- @Transactional 어노테이션 활성화 설정 --> <tx:annotaion-driven transaction-manager="transactionManager" /> | cs |
스프링이 제공하는 트랜잭션 기능을 사용하려면 PlatformTransactionManager 인터페이스를 이용해서
트랜잭션 관리자 객체를 만들어야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean { private DataSource dataSource; public void setDataSource(DataSource dataSource) { if (dataSource instanceof TransactionAwareDataSourceProxy) { // If we got a TransactionAwareDataSourceProxy, we need to perform transactions // for its underlying target DataSource, else data access code won't see // properly exposed transactions (i.e. transactions for the target DataSource). this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource(); } else { this.dataSource = dataSource; } } /** * Return the JDBC DataSource that this instance manages transactions for. */ public DataSource getDataSource() { return this.dataSource; } | cs |
DataSourceTransactionManager 클래스에는 DataSource 필드와 setter, getter 가 있기 때문에,
PlatformTransactionManager 인터페이스를 구현한 DataSourceTransationManager 클래스를 빈으로 등록하였다.
<tx:annotation-driven> 태그는 @Transactional 어노테이션이 붙은 메소드를 트랜잭션 범위에서 실행하는 기능을 활성화한다.
transaction-manager 속성을 사용하여 트랜잭션을 수행할 때 사용할 PlatformTransactionManager 빈(id 속성 값)을 지정한다.
<tx:annotation-driven> 태그의 주요 속성
transaction-manager |
사용할 PlatformTransactionManger 빈의 이름, 기본 값은 transactionManager |
proxy-target-class |
클래스에 대해서 프록시를 생성할 지의 여부 true 일 경우 클래스가 인터페이스를 구현했는지의 여부에 상관 없이 클래스를 상속받아 프록시를 생성 기본값은 false, 이 경우 인터페이스를 이용해서 프록시 생성 |
order |
Advice 적용 순서, 기본값은 int 의 최대값(가장 낮은 순위) |
자바 기반의 스프링이 설정을 사용하는 경우는 <tx:annotation-driven> 대신 @EnableTransactionManagement 어노테이션 사용
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | @Configuration @EnableTransactionManagement public class AppConfig { @Bean(destroyMethod = "close") public DataSource dataSource() { ComboPooledDataSource ds = new ComboPooledDataSource(); try { ds.setDriverClass("com.mysql.jdbc.Driver"); } catch (PropertyVetoException e) { throw new RuntimeException(e); } ds.setJdbcUrl("jdbc:mysql://localhost/spring4fs"); ds.setUser("spring4"); ds.setPassword("spring4"); return ds; } @Bean public MemberDao memberDao() { return new MemberDao(dataSource()); } @Bean public PlatformTransactionManager transactionManager() { DataSourceTransactionManager tm = new DataSourceTransactionManager(); tm.setDataSource(dataSource()); return tm; } /* @EnableTransactionManager 어노테이션의 속성 proxyTargetClass 클래스를 이용해서 프록시를 생성할지 지정, 기본값은 false 로 이 경우 인터페이스를 이용해서 프록시 생성 order AOP 적용 순서를 지정, 기본값은 가장 낮은 우선순위에 해당하는 int 의 최대값 */ | cs |
(2) @Transactional 사용
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class ChangePasswordService { private MemberDao memberDao; public ChangePasswordService(MemberDao memberDao) { this.memberDao = memberDao; } // 비지니스 로직이 실행되는 단위가 메소드이기 때문에 // 메소드에 @Transactional 을 붙여 트랜잭션 작업 단위로 설정 @Transactional public void changePassword(String email, String oldPwd, String newPwd) { Member member = memberDao.selectByEmail(email); if (member == null) throw new MemberNotFoundException(); member.changePassword(oldPwd, newPwd); memberDao.update(member); } } // memberDao.selectByEmail(), member.changePassword() // 두 메소드에서 실행되는 두 개의 쿼리가 하나로 묶여 트랜잭션 범위가 된다. | cs |
(3) Log4j 를 이용한 트랜잭션 확인
Log4j 는 로그 메시지를 남기기 위해 사용되는 로깅 프레임워크이다.
스프링은 내부적으로 JCL(Java Commons Logging) API 를 이용해서 로그를 남기는데, JCL 은 Log4j 모듈이 존재하면 Loj4j 를 이용해서
로그를 기록한다. Log4j 모듈을 추가하면 스프링이 JCL API 를 이용해서 남기는 로그 메시지를 Logj 설정에서 지정한 곳에 기록할 수 있다.
Maven Repository 에서 apache log4j 를 검색해서 pom.xml 에 등록하고,
로그 메시지를 어떤 형식으로 어디에 기록할지 Log4j 설정 파일을 만든다.
프로젝트를 Spring Legacy Project -> Spring MVC Project 로 만들었다면, src/main/resources 안에 log4j.xml 파일이 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <!-- log4j.xml --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <appender name="console" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="[%t] [%d{yyyy-MM-dd HH:mm:ss}] %-5p %c:%M - %m%n" /> </layout> </appender> <root> <priority value="INFO" /> <appender-ref ref="console" /> </root> <logger name="org.springframework.jdbc"> <level value="DEBUG" /> </logger> </log4j:configuration> | cs |
@Transactional 이 붙은 메소드가 실행될 수 있게 메인 메소드를 만들고 실행해 보면, 콘솔에 로그 메시지가 찍힌다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class MainForCPS { public static void main(String[] args) { AbstractApplicationContext ctx = new GenericXmlApplicationContext("classpath:appCtx.xml"); ChangePasswordService cps = ctx.getBean("changePwdSvc", ChangePasswordService.class); try { cps.changePassword("madvirus@madvirus.net", "1234", "1111"); System.out.println("암호를 변경했습니다."); } catch (MemberNotFoundException e) { System.out.println("회원 데이터가 존재하지 않습니다."); } catch (IdPasswordNotMatchingException e) { System.out.println("암호가 올바르지 않습니다."); } ctx.close(); } } | cs |
DataSourceTransactionManager 가 doBegin 하고, JDBC transaction 을 위해 필요한 커넥션은 ... 이고,
commit 은 manual 로 하며, Initiating transaction commit 을 해서 Committing JDBC transaction 을 했다.
트랜잭션이 시작되고 커밋한다는 내용이다.
트랜잭션을 롤백했을 때의 로그 메시지
3. 스프링의 PlatformTransactionManager
① ChangePasswordService 클래스의 메소드에 @Transactional 어노테이션이 붙어 있기 때문에,
스프링은 이 클래스에 트랜잭션 기능을 적용한 프록시 객체를 생성한다.
② 그렇기 때문에, getBean("changePwdSvc", ChangePasswordService.class) 로 해당 클래스의 빈 객체를 가져올 때,
ChangePassswordService 객체 대신 트랜잭션 처리를 위해 생성한 프록시 객체를 리턴한다.
③ 이 프록시 객체는 @Transactional 어노테이션이 붙은 메소드가 호출되면,
PlatformTransanctionManager 를 사용해서 트랜잭션을 시작하고, 실제 객체의 메소드를 호출한다.
④ 성공적으로 실행되면 커밋을 실행한다.
(2) @Transactional 적용 메소드의 롤백 처리
암호가 맞지 않아서 메인 클래스에서 처리한 IdPasswordNotMatchingException 이 발생하여 트랜잭션이 롤백 되었다.
프록시 객체는 원본 객체의 메소드를 실행하는 과정에서 RuntimeException 이 발생하면 트랜잭션을 롤백한다.
별도의 설정을 하지 않으면 RuntimeException 및 그 하위 타입의 예외에 대해 트랜잭션을 롤백한다.
그래서 트랜잭션의 롤백이 적용되게 하려면,
IdPasswordNotMatchingException 클래스를 작성할 때 RuntimeExcpetion 을 상속하도록 구현해야 한다.
JdbcTemplate 는 문제가 발생하면 RuntimeException을 상속한 DataAccessException 을 발생시키기 때문에,
JdbcTemplate 의 기능을 실행하는 도중 예외가 발생하면 프록시는 트랜잭션을 롤백시킨다.
SQLException 은 RuntimeException 을 상속하고 있지 않기 때문에, SQLException 이 발생하면 트랜잭션은 롤백되지 않는다.
SQLException 예외가 발생하는 경우에도 트랜잭션을 롤백하려면, @Transactional 의 rollbackFor 속성을 사용한다.
1 2 3 4 5 6 7 8 | @Transactional(rollbackFor = SQLException.class) public void someMethod(){ } // 여러 개의 예외 타입을 설정할 때는 배열로 지정 // @Transactional(rollbackFor = {SQLException.class, IOException.class}) // // rollbackFor 와 반대의 기능을 가지는 속성 -> noRollbackFor // 예외가 발생해도 롤백하지 않는다 | cs |
(3) @Transactional 의 주요 속성
속성 |
타입 |
|
value |
String |
트랜잭션을 관리할 때 사용할 PlatformTransactionMnager 빈의 이름을 지정 기본값은 "" 으로, 이 경우 <tx:annotation-driven> 이나 @EnableTransactionManagement 에서 지정한 트랜잭션 매니저를 사용. |
propagation |
Propagation |
트랜잭션 전파 타입을 지정, 기본값은 Propagation.REQUIRED |
isolation |
Isolation |
트랜잭션 격리 레벨을 지정, 기본값은 Isolation.DEFAULT |
timeout |
int |
트랜잭션의 제한 시간을 초 단위로 지정 기본값은 -1 로, 이 경우 데이터베이스의 타임아웃 시간을 사용 |
Propagation 열거 타입의 주요 값
REQUIRED |
메소드를 수행하는 데 트랜잭션이 필요하다는 것을 의미 현재 진행 중인 트랜잭션이 존재하면 해당 트랜잭션을 사용, 존재하지 않으면 새로운 트랜잭션을 생성 |
MANDATORY |
메소드를 수행하는 데 트랜잭션이 필요하다는 것을 의미 REQUIRED 와 달리 진행 중인 트랜잭션이 존재하지 않으면 예외가 발생 |
REQUIRED_NEW |
항상 새로운 트랜잭션을 시작 기존 트랜잭션이 존재하면 트랜잭션을 일시 중지하고 새로운 트랜잭션을 시작 새로 시작된 트랜잭션이 종료되면 기존 트랜잭션이 계속 진행 |
SUPPORTS |
메소드가 트랜잭션을 필요로 하지는 않지만, 기존 트랜잭션이 존재할 경우 트랜잭션을 사용한다는 것을 의미 진행 중인 트랜잭션이 존재하지 않더라도 메소드는 정상적으로 동작 |
NOT_SUPPORTED |
메소드가 트랜잭션을 필요로 하지 않음을 의미 SUPPORT 와 달리 진행 중인 트랜잭션이 존재하면 메소드가 실행되는 동안 트랜잭션은 일시 중지되며 메소드 실행이 종료된 후에 트랜잭션이 계속 진행 |
NEVER |
메소드가 트랜잭션을 필요로 하지 않으며, 만약 진행 중인 트랜잭션이 존재하면 예외가 발생 |
NESTED |
기존 트랜잭션이 존재하면, 기존 트랜잭션에 중첩된 트랜잭션에서 메소드를 실행 기존 트랜잭션이 존재하지 않으면, REQUIRED 와 동일하게 동작 이 기능은 JDBC 3.0 드라이브를 사용할 때만 적용 JTA Provider 가 이 기능을 지원할 경우에도 사용 가능 |
Isolation 열거 타입의 주요 값
DEFAULT |
기본 설정을 사용 |
READ_UNCOMMITTED |
다른 트랜잭션에서 커밋하지 않은 데이터를 읽을 수 있다 |
READ_COMMITTED |
다른 트랜잭션에 의해 커밋된 데이터를 읽을 수 있다 |
REPEATABLE_READ |
처음에 읽어 온 데이터와 두 번째 읽어 온 데이터가 동일한 값을 갖는다 |
SERIALIZABLE |
동일한 데이터에 대해서 동시에 두 개 이상의 트랜잭션이 수행될 수 없다 |
4. 트랜잭션 전파
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <!-- 스프링 설정 --> <bean id="someSvc" class="spring.SomeService"> <property name="anyService" ref="anySvc"/> </bean> <bean id="anySvc" class="spring.AnyService"/> <tx:annotation-driven/> // 자바 코드 public class SomeService{ private AnyService anyService; @Transactional public void some(){ any(); } public void setAnyService(AnyService as){ this.anyService = as; } } public class AnyService{ @Transactional public void any(){ } } /* @Transactional 의 propagation 속성의 기본값은 Propagation.REQUIED 로 진행 중인 트랜잭션이 존재하면 해당 트랜잭션을 사용하고 존재하지 않으면 새로운 트랜잭션을 생성한다. 그렇기 때문에, some() 메소드를 호출할 때 트랜잭션이 새로 생성된다. 그리고 some() 메소드 내부에서 any() 메소드를 호출하면 이미 some() 메소드에 의한 트랜잭션이 존재하기 때문에, some() 메소드 내부에서 any() 메소드가 호출될 때는 트랜잭션을 새로 생성하지 않고 some() 메소드에 의해 생성된 트랜잭션이 그대로 사용된다. ==> some() 메소드와 any() 메소드가 하나의 트랜잭션으로 묶여서 실행 any() 메소드에 @Transactional 이 없어도 some() 메소드 안에서 any() 메소드가 실행되면 some() 메소드에 의해 생성된 트랜잭션에 묶여서 커밋, 롤백의 적용 대상이 된다. propagation 속성값을 REQUIRED_NEW 로 설정하면, 기존의 트랜잭션이 존재하는지 여부에 상관없이 항상 새로운 트랜잭션이 시작된다. 이 경우에는 some() 메소드에 의해 트랜잭션이 생성되고, 다시 any() 메소드에 의해 새로운 트랜잭션이 생성된다. ==> 하나의 트랜잭션으로 묶이지 않는다 */ | cs |
'Programming > Spring' 카테고리의 다른 글
Spring MVC 프레임워크 동작 방식 (0) | 2017.08.23 |
---|---|
Spring MVC 기본 설정 (0) | 2017.08.23 |
Spring JdbcTemplate Method (0) | 2017.08.22 |
데이터 변환 (0) | 2017.08.20 |
다국어 처리 (0) | 2017.08.20 |
댓글