티스토리 뷰

IT/개발

Spring @Transactional 사용하기

K.Nero 2018. 10. 18. 09:46

이번 프로젝트에서 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 를 interface 로 만들었다.

@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
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
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
글 보관함