Hyun
Hyun
웹서비스 만들기를 좋아하는 개발자입니다.

SpringBoot JPA Multiple Datasource 환경에서 Transaction 처리

이번에 업무를 진행하다 Multiple Datasource 를 사용하게 되었는데,
둘 다 JPA를 활용하여 ORM 으로 사용해보려고 하던 중 Dirty Checking
의도대로 동작하지 않는 문제가 있었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Transactional
public void updateTriggerHookingLogStatus(long triggerHookingIdx, String status) {
    repository.findById(triggerHookingIdx)
              .get()
              .update(status);
}

...
@Entity
public class TriggerHookingLog {
  ...

  public void update(String status) {
    this.status = status;
  }
}

위 코드는 하나의 JPA를 auto-configuration 으로 사용할 때는 문제 없었는데,
이번에 Multiple Datasource로 사용하면서 의도대로 동작하지 않는 이슈가 있었다.

먼저 아래 Configuration을 살펴보자.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/// mysql jpa configuration
@Configuration
@EnableJpaRepositories(
    entityManagerFactoryRef = "mysqlEntityManager",
    transactionManagerRef = "mysqlTransactionManager",
    basePackages = {"com.uneedcomms.triggerprocessingservice.domain.mysql"}
)
@RequiredArgsConstructor
public class MysqlConfiguration {
    private final MysqlProperties mysqlProperties;

    @Bean
    @Primary
    public DataSource mysqlDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl(mysqlProperties.getUrl());
        dataSource.setUsername(mysqlProperties.getUsername());
        dataSource.setPassword(mysqlProperties.getPassword());
        dataSource.setDriverClassName(mysqlProperties.getDriverClassName());
        return dataSource;
    }

    @Bean(name = "mysqlEntityManager")
    @Primary
    public LocalContainerEntityManagerFactoryBean mysqlEntityManager(EntityManagerFactoryBuilder builder) {
        Map<String, String> properties = new HashMap<>();
        properties.put(JpaConstant.NAMING_STRATEGY, JpaConstant.DEFAULT_NAMING);
        properties.put(JpaConstant.DIALECT, JpaConstant.MYSQL_DIALECT);
        return builder
            .dataSource(mysqlDataSource())
            .packages("com.uneedcomms.triggerprocessingservice.domain.mysql")
            .properties(properties)
            .persistenceUnit("mysql")
            .build();
    }

    @Bean(name = "mysqlTransactionManager")
    @Primary
    PlatformTransactionManager mysqlTransactionManager(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(mysqlEntityManager(builder).getObject());
    }


/// postgresql jpa configuration
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
    entityManagerFactoryRef = "postgresqlEntityManager",
    transactionManagerRef = "postgresqlTransactionManager",
    basePackages = {"com.uneedcomms.triggerprocessingservice.domain.postgresql"}
)
@RequiredArgsConstructor
public class PostgresqlConfiguration {
    private final PostgresqlProperties postgresqlProperties;

    @Bean
    public DataSource postgresqlDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl(postgresqlProperties.getUrl());
        dataSource.setUsername(postgresqlProperties.getUsername());
        dataSource.setPassword(postgresqlProperties.getPassword());
        dataSource.setDriverClassName(postgresqlProperties.getDriverClassName());
        return dataSource;
    }

    @Bean(name = "postgresqlEntityManager")
    public LocalContainerEntityManagerFactoryBean postgresqlEntityManager(EntityManagerFactoryBuilder builder) {
        Map<String, Object> properties = new HashMap<>();
        properties.put(JpaConstant.NAMING_STRATEGY, JpaConstant.DEFAULT_NAMING);
        properties.put(JpaConstant.DIALECT, JpaConstant.POSTGRESQL_DIALECT);

        return builder
            .dataSource(postgresqlDataSource())
            .packages("com.uneedcomms.triggerprocessingservice.domain.postgresql")
            .properties(properties)
            .persistenceUnit("postgres")
            .build();
    }

    @Bean(name = "postgresqlTransactionManager")
    PlatformTransactionManager postgresqlTransactionManager(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(postgresqlEntityManager(builder).getObject());
    }
}

JPA를 Multiple Datasource로 사용하기 위해서는
수동으로 configuration을 해줘야 하는 불편함이 있다.

위와 같이 설정하면, 여러개의 Datasource를 JPA로 사용할 수 있게 된다.
즉, 각각의 entityManager, transactionManager 를 만들어서 사용하게 된다.

이렇게 여러개의 Datasource를 참조하는 상황에서는
반드시 명시적으로 entitiyManager, transactionManager 를 사용해야 한다.

1
2
3
4
5
// ex: entityManager 명시적 사용
@PersistenceContext(unitName = "postgres") // unitName 으로 구분

// ex: transactionManager 명시적 사용
@Transactional("postgresqlTransactionManager") // beanName 으로 구분

Multiple Datasource 환경에서 @Transactional 을 특별한 명시없이
일반적으로 사용하게 되면 @Primary로 설정한 TransactionManager를 참조하게 된다.

postgresql의 트랜잭션을 처리해야 하는데,
mysql의 트랜잭션을 가져오게 되어서 정상적인 동작이 되지 않는 것이다!

어떻게 보면 당연한 결과인 것인데, 평소 auto-configuration의 편리함에 의존하다 보니
놓친 부분이 있는 것 같다. (그래도 의존하고 싶다.. 설정 귀찮아.. 더 잘 만들어줘)