본문 바로가기
Programming/Spring

Spring JdbcTemplate Transaction

by TinKerBellBass 2017. 8. 22.
728x90
반응형

참고도서

초보 웹 개발자를 위한 스프링4 프로그래밍 입문
국내도서
저자 : 최범균
출판 : 가메출판사 2015.03.02
상세보기



1. 트랜잭션(Transaction)?

두 개의 쿼리를 사용해야 하나의 비지니스 로직을 수행할 수 있을 때, 첫 번째 쿼리는 성공했으나 두 번째 쿼리는 실패하였다.
첫 번째 쿼리는 데이터베이스에 반영되고, 두 번째 쿼리는 반영되지 않아서 문제가 발생하게 된다.
두 번째 쿼리를 실패하였으면 첫 번째 쿼리의 결과가 데이터베이스에 반영 되어서는 안된다.
트랜잭션은 여러 개의 쿼리를 사용해서 비지니스 로직을 수행해야 할 때, 이 쿼리들을 묶는 하나의 논리적인 작업 단위이다.
트랜잭션으로 묶인 쿼리 중 하나라도 실패하면 모든 실행 결과를 취소하여 기존의 상태로 되돌리고,
모든 쿼리가 성공하면 모든 실행 결과를 데이터베이스에 반영한다.
기존의 상태로 되돌리는 것을 롤백(rollback)이라고 하고, 데이터베이스에 실제로 반영하는 것을 커밋(commit)이라고 한다.

트랜잭션을 시작하면, 트랜잭션을 커밋하거나 롤백할 때까지 실행한 쿼리가 하나의 작업 단위가 된다.
JDBC 의 경우 setAutoCommit(false) 를 이용해서 트랜잭션을 시작하고, 
commit(), rollback() 을 이용해서 트랜잭션을 커밋하거나 종료한다. 

개발자가 트랜잭션을 코드로 관리하면 커밋과 롤백의 코드를 누락하는 실수를 범할 수도 있고, 
커밋과 롤백의 코드를 각각의 비지니스 로직을 구현할 때마다 중복해서 코딩해야 한다.
스프링을 이용하면 구조적 중복이 없는 매우 간단한 코드로 트랜잭션 범위를 지정할 수 있다.

2. @Transactional

(1) 스프링 설정파일 설정

namespace 에 tx 설정

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

(1) @Transactional 과 프록시

스프링은 @Transactional 어노테이션을 이용해서, 공통기능인 트랜잭션을 처리하기 위해 내부적으로 AOP 를 사용한다.
AOP 는 프록시를 통해서 구현되기 때문에 스프링에서의 트랜잭션 처리도 프록시를 통해서 이루어진다.
<tx:annotation-driven> 태그를 설정하면 스프링은 @Transactional 어노테이션이 적용된 빈 객체를 찾아 알맞은 프록시 객체를 생성한다.

① 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


728x90
반응형

'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

댓글