SpringFramework 트랜잭션 적용하기 @Transactional, <tx:annotation-driven>
트랜잭션에 대해 실습을 진해아다 보니 이전 어느 회사의 기술면접에서 스프링 프레임워크의 트랜잭션 기능에 대해서 설명해 보란 기억에 말문이 막혔던 기억이 난다. 이를 계기로 토비의 스프링을 1회독 하게 되었고 지금은 스프링에 대한 이해도가 이전과 비교한다면 확실히 넓어진 느낌이다.
각설하고, 스프링 트랜잭션 실습에 들어가기에 앞서 트랜잭션이란 과연 무엇일까 어렴풋이 알고는 있지만 용어의 모호함을 없애기 위해 사전적 정의를 살펴보고자 한다.
데이터 베이스에서 하나의 논리적 작업을 수행하기 위한 단위로서, 데이터 베이스 시스템에서 복구 및 병행시행 시 처리되는 작업의 논리 단위이다. 하나의 트랜잭션은 commit되거나 rollback된다. 트랜잭션은 일반적으로 회복의 단위가 된다.
__지형 공간정보체계 용어사전
예를들면 어떠한 작업을 수행하기 위해 A, B, C라는 절차를 수행해야 하고 그 중 하나라도 실패하면 안 될 경우
A->B->C 절차를 통틀어 트랜잭션이라 칭한다. 즉, 어떠한 업무를 수행하기 위한 회복 가능한 절차 단위가 트랜잭션이다.
다음은 트랜잭션의 ACID라 불리는 특징이다.
원자성(Atomicity)
|
하나의 트랜잭션은 모두 하나의 단위로 처리해야 한다. A와 B 작업이 하나의 트랜잭션으로 묶여있는 경우 A는 성공, B는 실패할 경우 해당 작업단위는 실패로 끝나야한다. 즉, A,B 모두 rollback 되어야 한다는 원칙 |
일관성(Consistency) |
트랜잭션이 성공했다면 데이터베이스의 모든 데이터는 일관성을 유지해야한다. |
격리성(Isolation) |
트랜잭션으로 처리되는 중간에 외부에서의 간섭은 없어야한다. |
영속성(Durability) |
트랜잭션이 성공적으로 처리되면, 그 결과는 영속적으로 보관되어야 한다. |
스프링에서는 aop기술을 이용해 트랜잭션을 적용하고 트랜잭션의 적용관련 설정을 더 간편히 하기위해 tx 네임스페이스를 제공한다. 오늘 진행할 실습에서는 tx 네임스페이스를 이용해서 유저 데이터의 추가 및 유저수를 증가시키는 것을 하나의 트랜잭션으로 하여 실습을 진행하고자 한다.
실습진행순서
0. Table 생성
1. 관련 Dependency 추가
2. DataAccess관련 설정 및 빈 추가 및 tx네임스페이스 추가
3. DAO 및 Service 생성 및 트랜잭션이 필요한 곳에 @Transactional 어노에티션 추가
4. Transaction 여부 테스트
0. Table 생성
말도안되는 테이블이라고 할 수 있지만 순전히 트랜잭션 테스트를 위함을 기억하자.. ㅜㅜ
CREATE TABLE `statistic` (
`tablename` varchar(45) NOT NULL,
`count` int(11) DEFAULT NULL,
PRIMARY KEY (`tablename`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
Insert into statistic(tablename, count) values('user',0);
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(45) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8
1. 관련 Dependency 추가(트랜잭션 사용을 위함)
[pom.xml]
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${org.springframework-version}</version>
</dependency>
2. DataAccess관련 설정 및 빈 추가 및 tx네임스페이스 추가
[IT/SpringFramework] - Spring MVC- MyBatis 연동하기
MyBatis설정 및 Spring연동은 위의 링크를 참고하고 이에 더해서 설정파일에 tx네임스페이스를 추가 및
<tx:annotation-driven/> 태그를 추가한다.
[datasource-context.xml]
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
<context:property-placeholder
location="classpath:properties/db.properties" />
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${db.driver}"></property>
<property name="url" value="${db.url}"></property>
<property name="username" value="${db.username}"></property>
<property name="password" value="${db.password}"></property>
</bean>
<bean id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="configLocation"
value="classpath:/config/mybatis-config.xml" />
<property name="mapperLocations"
value="classpath:/sqlmappers/**/*_SQL.xml" />
</bean>
<bean id="sqlSessionTemplate"
class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource" />
</bean>
<tx:annotation-driven />
</beans>
[mybatis.config]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="cacheEnabled" value="true" />
<setting name="lazyLoadingEnabled" value="false" />
<setting name="multipleResultSetsEnabled" value="true" />
<setting name="useColumnLabel" value="true" />
<setting name="useGeneratedKeys" value="false" />
<setting name="defaultExecutorType" value="SIMPLE" />
<setting name="defaultStatementTimeout" value="25000" />
</settings>
<!-- Value Object 설정 -->
<typeAliases>
<typeAlias alias="hashmap" type="java.util.HashMap" />
<typeAlias alias="Int" type="java.lang.Integer" />
<typeAlias alias="User" type="com.copocalypse.www.vo.User" />
</typeAliases>
</configuration>
[user_SQL.xml]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="user">
<select id="selectUser" parameterType="Int" resultType="User">
SELECT id,name,age from user
where id=#{id}
</select>
<insert id="insertUser" parameterType="User">
INSERT INTO user(name, age)
values(#{name},#{age})
</insert>
<update id="updateStatistic">
update statistic set count=count+1
where tablename='user'
</update>
<select id="selectStatistic" resultType="Int">
select count from statistic
where tablename='user'
</select>
</mapper>
3. DAO 및 Service 생성 및 트랜잭션이 필요한 곳에 @Transactional 어노에티션 추가
[UserDao.java]
public interface UserDao{
public User getUser(int uerId);
public int addUser(User user);
public int addStatistic();
public int getStatistic();
}
[UserDaoImpl.java]
@Repository("userDao")
public class UserDaoImpl implements UserDao {
@Autowired
SqlSession sqlSession;
@Override
public User getUser(int uerId) {
User user = sqlSession.selectOne("user.selectUser", uerId);
return user;
}
@Override
public int addUser(User user) {
int result=sqlSession.insert("user.insertUser", user);
return result;
}
@Override
public int addStatistic() {
int result=sqlSession.update("user.updateStatistic");
return result;
}
@Override
public int getStatistic() {
int result=sqlSession.selectOne("user.selectStatistic");
return result;
}
}
[UserService.java]
public interface UserService {
@Transactional
public void addUser(User user);
}
[UserServiceImpl.java]
@Service
public class UserServiceImpl implements UserService{
@Autowired
UserDao userDao;
@Override
public void addUser(User user) {
userDao.addUser(user);
userDao.addStatistic();
}
}
4. Transaction 여부 테스트
트랜잭션 적용 유무를 확인하기 위해 테스트를 위한 클래스를 하나 만들고 해당 UserServiceImpl클래스를 상속한 빈으로 설정하여 테스트를 진행하였다.
트랜잭션의 경우 RuntimeException이 발생할 경우 rollback함을 확인 할 수 있다. 그런데 이상하게 RuntimeException이 발생하였는데도 콜백이 안되었는데 해당 이유에 대해서는 테스트 메소드 위에 주석을 달아놨다.
[UserServiceTest.java]
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = { "classpath:config/*-context.xml", "classpath:web-context.xml" })
public class UserServiceTest {
@Autowired
@Qualifier(value = "testUserService")
UserService testUserService;
@Autowired
UserDao userDao;
//해당 메서드에 @Transactional 어노테이션을 붙였었으나 그로인해 트랜잭션이 이 메소드로부터 실행되어 예외가 발생하여도
//롤백이 되지 않는 현상이 발생하였음. 테스트를 제대로 하기위해 @Transactional 어노테이션 없앰.
//단, 트랜잭션 전파속성을 조정하면 @Transactional 어노테이션을 붙여놓아도 될 듯함.
@Test
public void addUserTest() {
int sStatistic=userDao.getStatistic();
User user = new User();
user.setName("한승연");
user.setAge(20);
try {
testUserService.addUser(user);
fail("Exception existing in the addUser method()");
}catch(TestUserServiceException e) {
System.out.println("error exist");
}
int eStatistic=userDao.getStatistic();
System.out.println(eStatistic);
assertThat(0, is(eStatistic-sStatistic));
}
@Service("testUserService")
static class TestUserService extends UserServiceImpl{
private String name="한승연";
@Override
public void addUser(User user) {
super.addUser(user);
if(user.getName().equals("한승연")) throw new TestUserServiceException();
}
}
static class TestUserServiceException extends RuntimeException{}
}