Spring Legacy @Transactional 동작하지 않는 문제 (tx:annotation-driven)
Spring Legacy Project @Transactional 동작하지 않는 문제
해당 포스팅은 '스프링 프로젝트에서 @Tranactional 어노테이션이 동작하지 않는 문제를 해결'하며 정리한 내용입니다.
결론적으로 트랜잭션이 동작하지 않았던 원인은 'root-context.xml' 파일과 'servlet-context.xml' 파일에 대한 설정이 잘못되었기 때문이었는데요.
* 그중에서도 '<context:component-scan/>'
아래 내용을 통해 트랜잭션이 동작하지 않았을 때의 설정과 어떻게 수정하여 해결하였는지 살펴보겠습니다.
트랜잭션이 동작하지 않을 때 설정
<?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:tx="http://www.springframework.org/schema/tx"
xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<!-- Root Context: defines shared resources visible to all other web components -->
<!-- for mysql -->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/spring_mvc?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true" />
<property name="username" value="root" />
<property name="password" value="" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:/sqlmap/**/*_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">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 어노테이션 기반 트랜잭션 설정 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
(root-context.xml)
Spring Proejct에서 트랜잭션을 사용하는 방법을 찾아보니 대부분 다음과 같이 root-context.xml 파일에 'transactionManager bean'과 '<tx:annotation-driven/>'을 설정하여 트랜잭션을 적용하는 것을 볼 수 있었습니다.
(pom.xml 파일에 spring-tx 의존성이 등록된 상태에서 진행하였습니다.)
* tx:annotation-driven은 default 값으로 transactionManager라는 이름으로 선언된 PlatformTransactionManager를 찾습니다. 때문에 TransactionManager가 빈으로 등록된 이름이 transactionManager가 아닌 경우에만 transaction-manager="bean name" 부분을 추가해도 됩니다.
DEBUG: org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
DEBUG: org.mybatis.spring.SqlSessionUtils - SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4a1b5152] was not registered for synchronization because synchronization is not active
DEBUG: org.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
DEBUG: org.mybatis.spring.transaction.SpringManagedTransaction - JDBC Connection [129572341, URL=jdbc:mysql://localhost:3306/spring_mvc?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true, UserName=root@localhost, MySQL Connector/J] will not be managed by Spring
...(생략)
DEBUG: org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4a1b5152]
하지만 위와 같은 설정 상태에서 Service 단의 @Transactional 어노테이션이 적용된 메서드를 실행했을 때 트랜잭션이 동작하지 않았으며, 위 로그와 같이 'was not registered for synchronization because synchronization is not active', 'Closing non transactional SqlSession' 메시지가 뜨는 것을 확인할 수 있었는데요.
수정 후 트랜잭션이 동작할 때 설정
<?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:tx="http://www.springframework.org/schema/tx"
xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<!-- Root Context: defines shared resources visible to all other web components -->
<!-- for mysql -->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/spring_mvc?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true" />
<property name="username" value="root" />
<property name="password" value="9269" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:/sqlmap/**/*_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">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
(root-context.xml)
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<!-- DispatcherServlet Context: defines this servlet`s request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="com.example.board" />
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans:beans>
(servlet-context.xml)
다음과 같이 root-context.xml에서 '<tx:annotation-driven/>' 설정을 빼고 servlet-context.xml에 해당 설정을 추가하였으며, 이후 아래와 같이 트랜잭션이 정상적으로 동작하는 것을 확인할 수 있었습니다.
DEBUG: org.springframework.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [com.example.board.service.BookServiceImpl2.transactionTest]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
...(생략)
DEBUG: org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
DEBUG: org.mybatis.spring.SqlSessionUtils - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33610473]
...(생략)
DEBUG: org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33610473]
DEBUG: org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33610473]
DEBUG: org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33610473]
DEBUG: org.springframework.jdbc.datasource.DataSourceTransactionManager - Initiating transaction rollback
DEBUG: org.springframework.jdbc.datasource.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [1140863135, URL=jdbc:mysql://localhost:3306/spring_mvc?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true, UserName=root@localhost, MySQL Connector/J]
DEBUG: org.springframework.jdbc.datasource.DataSourceTransactionManager - Releasing JDBC Connection [1140863135, URL=jdbc:mysql://localhost:3306/spring_mvc?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true, UserName=root@localhost, MySQL Connector/J] after transaction
원인은 잘못 설정된 context:component-scan
'<tx:annotation-driven/>' 설정이 root-context.xml에 있을 때는 트랜잭션이 동작하지 않고 servlet-context.xml에 있을 때는 트랜잭션이 동작하는 이유가 궁금했는데요.
결국 찾게 된 원인은 잘못 설정된 '<context:component-scan/>' 때문이었습니다.
원인을 찾아보던 중 '@Transactional 어노테이션이 사용되는 빈들은 root-context.xml에서 component-scan을 통해 빈으로 등록되어야 한다.'는 내용을 보게 되었는데요.
<tx:annotation-driven/> 설정이 존재하는 경우 등록된 빈 중에서 @Transactional 어노테이션이 붙은 클래스(또는 인터페이스 또는 메서드)를 스캔하고 해당 대상이 트랜잭션 처리가 될 수 있도록 적용합니다.
***
즉, tx:annotation-driven 설정이 적용될 때 @Transactional 어노테이션을 사용하는 대상은 이미 빈으로 등록된 상태여야 한다는 것인데요.
여기서 잠깐 web-context.xml 파일을 통해 root-context.xml 파일과 servlet-context.xml 파일이 로딩되는 순서를 간단하게만 이야기하자면, root-context.xml 파일이 먼저 로딩된 후 root-context.xml에 등록된 Spring Container가 구동됩니다.
이후 DispatcherServlet에 의해 servlet-context.xml 파일이 로딩됩니다.
때문에 최초 시도에서 tx:annotation-driven은 root-context.xml에서 이미 로딩되었는데 나중에 servlet-context.xml에서 component-scan으로 @Transactional 어노테이션이 있는 빈을 로딩했기 때문에 트랜잭션이 적용되지 않았던 것입니다.
최종 xml(root-context, servlet-context) 설정
<context:component-scan base-package="com.example.board">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
(root-context.xml 설정 중 일부)
<context:component-scan base-package="com.example.board" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>
(servlet-context.xml 설정 중 일부)
이후 root-context.xml 파일과 servlet-context.xml 파일에는 각각 어떤 설정이 들어가야 하는지를 찾아보니, 다음과 같이 servlet-context.xml 파일에서는 @Controller 관련된 빈을 스캔하고 root-context.xml 파일에서는 컨트롤러를 제외한 @Service, @Mapper, @Repository 등을 스캔한다는 것을 알 수 있었습니다.
* root-context.xml에서는 tx:annotation-driven 설정과 함께 @Transactional이 사용되는 @Service가 빈으로 등록되기 때문에 트랜잭션이 정상적으로 동작한다는 것을 알 수 있습니다.
마무리
해당 문제를 해결하는 과정에서 'transaction의 자세한 동작 순서'와 'web-context.xml, root-context.xml, servlet-context.xml 각각에는 어떤 설정이 들어가는지', '트랜잭션과 프록시' 등 이해가 부족한 부분을 많이 발견하게 되었는데요. 관련 내용들도 공부하여 정리한 내용을 아래에 연관 포스팅으로 링크하도록 하겠습니다.
내용 중 잘못된 부분이나 궁금하신 부분은 댓글 남겨주시면 확인하겠습니다. 감사합니다.