티스토리 뷰
이번 프로젝트에서 Transactional 을 사용하면서 우연히 알게 된 사실이 있다.
환경은 Spring + MyBatis + PostgreSQL 이고 MyBatis 설정은 아래와 같다.
@Configuration
@MapperScan(basePackages = "com.westudy.api.*",
sqlSessionFactoryRef = "Mybatis_PostgreSQL_SqlSessionFactory",
sqlSessionTemplateRef = "Mybatis_PostgreSQL_SqlSessionTemplate")
@ConfigurationProperties("spring.datasource")
@EnableTransactionManagement
public class MyBatisConfig extends HikariDataSource {
@Bean("postgreSQLDataSource")
public DataSource dataSource() {
this.setAutoCommit(false);
return new HikariDataSource(this);
}
@Bean("Mybatis_PostgreSQL_SqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource());
return bean.getObject();
}
@Bean("Mybatis_PostgreSQL_SqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}
일단 Transactional을 사용하기 위해서 맨 밑에 TransactionManager 를 추가 했다.
그리고 메소드에서 트랜잭션을 사용하기 위해서 @Transactional 을 선언했다.
@Transactional
List<SearchReviewDto> getReview(SearchParam searchParam) {
log.info("Search condition: {}", searchParam);
List<SearchReviewDto> dtoList = reviewRepository.getReviewList(searchParam);
for (SearchReviewDto dto : dtoList) {
List<Map<String, Object>> score = reviewRepository.getReviewScore(dto.getId());
dto.setScore(calcScore(score));
}
return dtoList;
}
별 의심하지 않고 오픈하기 위해서 운영서버에 배포를 하고 모니터링을 하기 위해서 Scouter 를 연결했다. (참고로 저 메소드는 모두 Select 밖에 사용하지 않기 때문에 트랜잭션을 사용할 필요가 없다. 하지만 여러 개의 쿼리를 하나의 커넥션으로 수행하기 위해서 적용했다.) 그리고 한 건을 호출해서 xlog 를 확인해 봤다.
- [000004] 09:24:29.641 0 0 OPEN-DBC ....-- OPEN-DBC
[000004] [000005] 09:24:29.641 0 0 PRE> 0 <Affected Rows : 0> 4 ms
- [000006] 09:24:29.645 4 0 AutoCommit : false #0 0 ms
....
- [000013] 09:24:29.648 0 0 COMMIT [1ms] -- COMMIT
- [000014] 09:24:29.649 1 0 CLOSE [0ms] -- CLOSE
- [000015] 09:24:29.649 0 0 OPEN-DBC ...-- OPEN-DBC
- [000016] 09:24:29.649 0 0 AutoCommit : false #0 0 ms
- [000018] 09:24:29.650 1 0 RESULT-SET-FETCH #5 0 ms
- [000019] 09:24:29.650 0 0 COMMIT [1ms] -- COMMIT
- [000020] 09:24:29.651 1 0 CLOSE [0ms] -- CLOSE
- [000021] 09:24:29.651 0 0 ReviewService#calcScore() [0ms] -- com.westudy.api.review.ReviewService.calcScore(Ljava/util/List;)Ljava/util/List;
- [000022] 09:24:29.651 0 0 OPEN-DBC ....-- OPEN-DBC
....
- [000026] 09:24:29.652 0 0 COMMIT [0ms] -- COMMIT
- [000027] 09:24:29.652 0 0 CLOSE [0ms] -- CLOSE
- [000028] 09:24:29.652 0 0 ReviewService#calcScore() [0ms] -- com.westudy.api.review.ReviewService.calcScore(Ljava/util/List;)Ljava/util/List;
- [000029] 09:24:29.652 0 0 OPEN-DBC jdbc:postgresql:...[0ms]
...
생략을 좀 했지만 repository 를 사용할 때 마다 getConnection을 계속해서 호출하는 것을 보고 순간 당황했다. 원인을 찾기 위해서 BreakPoint 를 잡고 Spring 의 Proxy 들을 모두 찾아 다니며 어느 부분에서 처리를 하는지 확인을 해보니 config 에서 설정하는 SqlSessionTemplate 안의 SqlSession이 매번 생성되는 것을 확인할 수 있었다.
퇴근해서도 무한 구글링을 통해 두 가지의 해결방법을 확인하고 출근하자 마자 적용해 보았다.
1. class 에 @Transactional을 같이 선언해 준다.
2. method를 public으로 변경해 보자.
더 확률이 높을 것 같은 2번을 적용하고 테스트 해보니 정상적으로 나오는 것을 확인했다.
Transactional을 상용할 메소드는 public으로...ㅠㅠ
디버깅을 통해서 알게된 점
@Repository
public interface StatisticsRepository {
@Update("query....")void method(Param param);
}
이 인터페이스는 ibatis 의 MapperProxy로 구현이 되고 JdkDynamicAopProxy에 의해서 호출이 된다.
(JdkDynamicAopProxy -> ReflectiveMethodInvocation -> AopUtil -> MapperProxy 순으로 호출)
spring proxy 가 ibatis 가 만든 MapperProxy 를 호출하게 되는데 이 안의 SqlSession 도 스프링의 SqlSessionTemplate 이 구현체 이다. 이 안에서 jdk reflect.Proxy.newProxyInstance 를 사용하여 SqlSession의 Proxy 를 생성하고 InvocationHandler인 SqlSessionInterceptor 를 설정해 주는 것을 생성자에서 확인할 수 있다. 그리고 여기서 getSqlSession을 호출해서 가져오는데 이 안으로 들어가보면 SqlSessionHolder 를 가져오게 되면 커넥션을 재사용하게 되고 null을 반환할 경우 새로 생성하게 된다.
SqlSessionHolder 도 TransactionSynchronizationManager.getResource 에 의해서 가져오는 것도 확인할 수 있고 TransactionSynchronizationManager 의 doGetResource 의 map 이 비어 있는 것을 확인할 수 있다.
Map<Object, Object> map = resources.get();
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
결국! TransactionSynchronizationManager 의 bindResource 가 후출되어야지만 하나의 커넥션을 사용하여 트랜잭션을 사용할 수 있다는 것을 알았다. 이제 bindResource 가 어디서 호출되는지 확인해 보자.
@Transactional 메소드가 호출될 때를 확인해 보면 CglibAopProxy 를 생성하는데 이 객체를 생성할 때 @Transactional 메소드가 public 이어야 지만 CglibAopProxy 의 advised 에 chaninFactory 에서 interceptorAndDynamicMethodMatchers 로 TransactionalInterceptor 를 가져올 수 있다. 그리고 그 과정에서 bindResource 를 통해서 resources 에 객체가 저장된다. 이 객체가 ThreadLocal 이라에 저장되는 것도 중요하다.
'IT > 개발' 카테고리의 다른 글
Java8 Feature (0) | 2019.03.04 |
---|---|
local Docker 개발 환경 만들기 (0) | 2018.10.19 |
Spring + JWT (0) | 2018.08.31 |
객체지향 이란 (0) | 2018.06.10 |
레거시에 Solr 적용하기 (0) | 2018.05.22 |