지난 글에서는 ibatis, Mybatis를 연동하는 환경설정 파일인 context-sqlMap.xml, context-mapper.xml 파일을 Java Config 파일로 바꾸는 작업에 대해 설명하였다. 이번에는 이렇게 연동한 DB 관련 작업에 적용하는 트랜잭션 설정 파일인 context-transaction.xml을 Java Config 파일로 바꾸는 작업에 대해 설명하도록 하겠다.
전자정부 프레임워크의 트랜잭션 방법은 XML에서 tx 스키마를 이용하는 AOP 방식으로 구현하고 있다. 이 얘기를 왜 하냐면 Spring에서 XML을 이용해서 트랜잭션을 설정하는 방법이 두 가지가 있어서 그렇다.
- <tx:advice> 태그로 정의하는 어드바이스와 포인트컷을 이용하여 AOP로 구현하는 트랜잭션
- <tx:annotation-driven /> 태그를 사용하여 @Transactional 어노테이션이 붙은 클래스, 메소드에 구현하는 트랜잭션
<tx:annotation-driven /> 태그를 사용해도 내부적으로는 Proxy 방식의 Spring AOP가 적용되기 때문에 두 방법 모두 AOP를 사용한다고 보면 된다(태클을 거실 분이 있을것 같아서 먼저 말하자면 <tx:annotation-driven /> 태그 속성 중 mode 라는 속성이 있다. 이 속성의 기본값이 proxy인데 이 속성이 proxy일 경우엔 Spring AOP를 사용한다. 그러나 이 속성이 aspectj로 설정되어 있으면 AspectJ를 이용하는 트랜잭션을 사용하게 된다)
일단 이 정도의 선지식을 알아두고 현재 설정되어 있는 context-transaction.xml의 내용을 보도록 하자.
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="*" rollback-for="Exception"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="requiredTx" expression="execution(* egovframework.example.sample..impl.*Impl.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="requiredTx" />
</aop:config>
XML 내용을 보면 DataSource 기반의 TransactionManager인 DataSourceTransactionManager bean 을 선언한 뒤 이를 <tx:advice> 태그와 <aop:config> 태그들을 이용하여 트랜잭션을 구현하는 방식이다. 이 코드를 이해하는데는 별 문제가 없을 것 같고, 그럼 이와 같은 내용을 Java Config 방식으로 하는 방법에 대해 설명하도록 하겠다. DataSourceTransactionManager bean을 설정하는 <bean> 태그를 Java Config 방식으로 설정하는 방법은 최종 Java Config Source를 보여주는 것으로 하고 여기에서는 생략하겠다. 지금까지의 내용을 읽어왔으면 이 부분을 설정하는 것이 어렵지가 않기 때문이다.
먼저 <tx:advice> 태그를 이용하여 어드바이스 정의하고 이를 활용하는 AOP 방식의 트랜잭션을 Java Config 방식으로 변환하는 방법이 없다. 그래서 이러한 방법이 아니라 다른 방법으로 트랜잭션을 구현하는 전환이 필요하다. 위에서 보면 <tx:advice> 태그를 사용하는 방법 말고 <tx:annotation-driven /> 태그를 사용하여 트랜잭션을 할 수 있다고 설명한 부분이 있다. 이 방법으로 트랜잭션을 구현한다고 생각하고 이를 Java Config 방식으로 변환하도록 하자.
(추가 글을 적어둔다. 이 부분에 대해 댓글로 알려주신 분이 있어서 내용을 살펴보니 설정이 가능하다는 판단이 서서 변환하는 방법이 없다..는 식의 단적인 결론 내린 것은 수정하고자 한다. 댓글로 알려주신 분이 비밀댓글로 달아주셔서 다른 사람들이 볼 수 없기에 댓글로 달아주신 분에게 비밀댓글을 풀어달라고 요청을 드린 상태다. 만약 풀어지지 않을 경우엔 댓글에 쓰여진 참조 링크를 글에 명시하도록 하겠다)
Spring에서는 Java Config 방식으로 트랜잭션을 구현할 수 있는데, 이를 위해 사용되는 어노테이션이 @EnableTransactionManagement 어노테이션이다. 이 어노테이션을 사용하면 <tx:annotation-driven /> 태그를 사용하는 것과 동일한 설정을 하게 된다. 다만 차이점이 존재하는 부분이 있다. <tx:annotation-driven /> 태그의 경우는 해당 태그에 적용되는 Transaction Manager bean을 찾는 방법을 bean의 이름으로 찾게 된다. <tx:annotation-driven /> 태그에는 속성으로 transactionManager란 속성이 있는데 이 속성을 이용해서 사용하고자 하는 Transaction Manager bean 이름을 설정하게 된다. 이 속성의 기본값은 transactionManager 로 되어 있어 Transaction Manager bean의 이름을 transactionManager로 설정하면 <tx:annotation-driven /> 태그에서 transactionManager 속성을 지정하지 않아도 이용이 가능하다. 그러나 @EnableTransactionManagement는 이러한 Transaction Manager bean을 찾는 방법을 bean의 타입으로 찾게 된다. Spring에서 제공하는 Transaction Manager 클래스들은 PlatformTransactionManager 인터페이스를 구현하고 있는데 @EnableTransactionManagement 어노테이션은 등록되어 있는 Spring Bean 들 중에서 PlatformTransactionManager 인터페이스를 구현한 클래스타입의 bean을 찾아서 이를 Transaction Manager로 사용하게 된다. 즉 bean의 이름을 구애받지 않기 때문에 Transaction Manager bean의 이름을 어떤것을 사용하든(txManager, tm 등) 상관이 없다.
그러면 이 설정을 사용하면 <tx:annotation-driven /> 과 동일한 기능의 설정을 한다는것은 이제 알았으니 이제 Sample 프로젝트를 고쳐야 한다. 어노테이션 기반으로 트랜잭션을 하기 때문에 Sample 프로젝트의 소스 중 트랜잭션 대상이 되는 부분에 @Transactional 어노테이션을 붙여야 한다. context-transaction.xml의 설정을 보면 트랜잭션의 대상이 되는 포인트것이 * egovframework.example.sample..impl.*Impl.*(..) 이러한 패턴에 속하는 메소드이기 때문에 현재 Sample 프로젝트에서 이 패턴의 대상이 되는 클래스와 메소드가 egovframework.example.sample.service.impl Package에 있는 EgovSampleServiceImpl 클래스에 있는 모든 메소드가 그 대상이 된다. 이 클래스의 메소드에 @Transactional 어노테이션을 붙인다. 아래에서 보여주는 코드는 메소드의 세부 코드는 생략했으나 이렇게 메소드레벨에 @Transactional을 선언해주어 @EnableTransactionManagement 어노테이션 설정으로 인한 트랜잭션 적용을 받도록 해주었다.
public class EgovSampleServiceImpl extends EgovAbstractServiceImpl implements EgovSampleService {
@Transactional
@Override
public String insertSample(SampleVO vo) throws Exception {
...
}
@Transactional
@Override
public void updateSample(SampleVO vo) throws Exception {
...
}
@Transactional
@Override
public void deleteSample(SampleVO vo) throws Exception {
...
}
@Transactional(readOnly=true)
@Override
public SampleVO selectSample(SampleVO vo) throws Exception {
...
}
@Transactional(readOnly=true)
@Override
public List<?> selectSampleList(SampleDefaultVO searchVO) throws Exception {
...
}
@Transactional(readOnly=true)
@Override
public int selectSampleListTotCnt(SampleDefaultVO searchVO) {
...
}
}
일단 Java Config 방식으로 트랜잭션 설정하는 방법은 이정도에서 마치고 한가지 부가적인 설명을 하고자 한다. 사실 @Transactional 어노테이션을 이용하는 트랜잭션 설정은 완전 범용적인 설정은 아니다. 이 방법은 트랜잭션을 적용해야 할 클래스 또는 메소드의 Source에 @Transactional 어노테이션을 붙여야 하기 때문에 프로그래머가 Source에 직접 접근이 가능할때 사용이 가능한 방법이다. 그러나 외부에서 배포된 클래스를 대상으로 트랜잭션을 적용해야 할 경우엔 이 방법을 사용할 수가 없다. 그래서 이러한 상황일 경우엔 XML을 사용하여 <tx:advice> 태그와 <aop:config> 태그를 이용하는 방법으로 접근해야 한다. 그러나 이렇게 할 경우엔 Java Config와 XML 설정 파일이 혼용이 되는 상황이 온다. 그래서 이럴 경우엔 XML 설정 파일을 Java Config에서 사용하는 의미로 @ImportResource를 이용하여 Java Config 설정 파일에서 XML 설정 파일을 이용한 설정 작업을 진행하는 것으로 작업하도록 한다.(비슷한 것으로 @Import 어노테이션이 있는데 이 어노테이션은 XML 설정 파일이 아닌 다른 Java Config 방식의 자바 클래스를 사용하는 것으로 환경 설정 작업을 진행한다고 생각하면 된다. 이 어노테이션은 차후에 설명하도록 하겠다)
@ImportResource("classpath:/egovframework/spring/context-transaction.xml")
마지막으로 context-sqlMap.xml 파일을 Java Config 방식으로 변경한 ContextSqlMap 클래스 소스와 context-mapper.xml 파일을 Java Config 방식으로 변경한 ContextMapper 클래스 소스를 보여주면서 이 글을 마무리하겠다.
package egovframework.example.config.root;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
// @ImportResource("classpath:/egovframework/spring/context-transaction.xml")
public class ContextTransaction {
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager dstm = new DataSourceTransactionManager();
dstm.setDataSource(dataSource);
return dstm;
}
}