๐Ÿ’Š Java & Kotlin & Spring/- spring framework +

Multi Datasource Application ์„ธํŒ…ํ•˜๊ธฐ (with Spring Boot 3.3 + JPA + kotlin)

Wonit 2024. 9. 30. 21:35

๋ชฉ์ฐจ

  1. Multi DataSource Application ํ™˜๊ฒฝ
  2. ์‹ค์Šต ํ™˜๊ฒฝ ๊ตฌ์„ฑ
  3. Spring Application ๊ตฌ์„ฑ - 1. DataSource ๊ตฌ์„ฑ
  4. Spring Application ๊ตฌ์„ฑ - 2. EntityMangager TxManager ๊ตฌ์„ฑ

 

1. Multi DataSource Application ํ™˜๊ฒฝ

 

Spring Application ์„ ๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ๋ถ„๋ช… ํ•˜๋‚˜์˜ applcation ์—์„œ ๋‘๊ฐœ ์ด์ƒ์˜ DB ๋ฅผ ๋ฐ”๋ผ๋ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์กด์žฌํ•œ๋‹ค.

 

์ด๊ธฐ์ข… datasource ์„ค์ •

 

์ด๋ฒˆ ์‹ค์Šต์—์„œ๋Š” ํ•˜๋‚˜์˜ Spring Container ์—์„œ ๋‘๊ฐœ ์ด์ƒ์˜ DataSource ๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณผ ์˜ˆ์ •์ด๋‹ค.

 

2. ์‹ค์Šต ํ™˜๊ฒฝ ๊ตฌ์„ฑ

  • kotlin (jdk 21)
  • Spring Boot 3.3.4
  • Spring Data Jpa
  • MySQL & PostgreSQL
  • docker-compose

 

์šฐ์„  Database Server ๋Š” Docker Contianer ๋ฅผ ์‚ฌ์šฉํ•ด์„œ local ์—์„œ ์‹คํ–‰์‹œํ‚ฌ ์˜ˆ์ •์ด๋‹ค.

 

MySQL ๊ณผ PostgreSQL ์€ Application ์—์„œ ํ•ญ์ƒ ๋™์‹œ์— ๊ฐ™์ด ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋ฏ€๋กœ ์—ฌ๋Ÿฌ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ์— ์šฉ์ดํ•˜๋„๋ก docker-comopse ๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋‹ค.

 

์šฐ์„  docker-componse.yml ์„ ์ƒˆ๋กœ ๋งŒ๋“ค๊ณ  local ์—์„œ db ๋ฅผ ๋„์›Œ๋ณด์ž

 

version: "3.8"
services:
  order-db:
    image: postgres:14
    container_name: order-db
    environment:
      POSTGRES_DB: orderdb
      POSTGRES_USER: orderuser
      POSTGRES_PASSWORD: orderpassword
    ports:
      - "5432:5432"
    volumes:
      - ./sql/init-order.sql:/docker-entrypoint-initdb.d/init.sql

  delivery-db:
    image: mysql:8
    container_name: delivery-db
    environment:
      MYSQL_DATABASE: deliverydb
      MYSQL_USER: deliveryuser
      MYSQL_PASSWORD: deliverypassword
      MYSQL_ROOT_PASSWORD: deliverypassword
      MYSQL_CHARACTER_SET_SERVER: "utf8mb4"
      MYSQL_COLLATION_SERVER: "utf8mb4_unicode_ci"
    ports:
      - "3306:3306"
    volumes:
      - ./sql/init-delivery.sql:/docker-entrypoint-initdb.d/init.sql

networks:
  default:
    driver: bridge

 

container ๊ฐ€ ์‹คํ–‰๋  ๋•Œ inital data ๋ฅผ ํ•จ๊ป˜ ๋„ฃ์–ด์ฃผ์ž.

 

-- ./sql/init-delivery.sql
CREATE TABLE IF NOT EXISTS deliveries (
  id INT AUTO_INCREMENT PRIMARY KEY,
  delivery_name VARCHAR(255) NOT NULL,
  delivery_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO deliveries (delivery_name) VALUES ('์ฒซ ๋ฒˆ์งธ ๋ฐฐ์†ก'), ('๋‘ ๋ฒˆ์งธ ๋ฐฐ์†ก');


-- ./sql/init-order.sql
CREATE TABLE IF NOT EXISTS orders (
  id SERIAL PRIMARY KEY,
  order_name VARCHAR(255) NOT NULL,
  order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO orders (order_name) VALUES ('์ฒซ ์ฃผ๋ฌธ'), ('๋‘ ๋ฒˆ์งธ ์ฃผ๋ฌธ');

 

์šฐ๋ฆฌ๋Š” ์ด ๋‘๊ฐœ์˜ ์„œ๋กœ ๋‹ค๋ฅธ DB ์™€ ํ…Œ์ด๋ธ”์— ์ ‘๊ทผํ•˜๋Š” Application ์„ ๋งŒ๋“ค ๊ฒƒ์ด๋‹ค.

 

docker-compose up ์„ ํ†ตํ•ด container ๋“ค์„ ์‹คํ–‰์‹œํ‚ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ •์ƒ์ ์œผ๋กœ DB ๊ฐ€ ์‹คํ–‰๋œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค

 

 

3. Spring Application ๊ตฌ์„ฑ

์ด์ œ ์‹ค์Šต์„ ์œ„ํ•œ ํ™˜๊ฒฝ ๊ตฌ์„ฑ์ด ์™„๋ฃŒ๋˜์—ˆ๋‹ค.

 

Spring Application ์„ ๊ฐœ๋ฐœํ•ด๋ณด์ž.

 

๊ธฐ๋ณธ์ ์œผ๋กœ Spring Application ์˜ boilerplate ๋Š” ๋”ฐ๋กœ ์„ค๋ช…ํ•˜์ง€ ์•Š๊ฒ ๋‹ค. ์ „์ฒด ์†Œ์Šค์ฝ”๋“œ๊ฐ€ ๊ถ๊ธˆํ•˜๋‹ค๋ฉด https://github.com/my-research/distributed-transaction-basic ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

3-1. DataSource ์ƒ์„ฑ

 

์ผ๋ฐ˜์ ์œผ๋กœ ์šฐ๋ฆฌ๋Š” JPA ๋ฅผ ๊ตฌ์„ฑํ•  ๋•Œ spring-data-jpa-starter ์˜ auto configuration ์„ ์‚ฌ์šฉํ•œ๋‹ค.

 

์ด๋ฒˆ ์‹ค์Šต๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ auto configuration ์„ ํ•  ์˜ˆ์ •์ด์ง€๋งŒ default ๊ฐ€ ๋‹จ์ผ datasource ์ด๋ฏ€๋กœ ๊ธฐ์กด ๋ฐฉ์‹์—์„œ ์กฐ๊ธˆ ๋ณ€๊ฒฝํ•  ๊ฒƒ์ด๋‹ค.

 

spring.datasource ํ•˜์œ„์— order ์™€ delivery ๋ฅผ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

 

spring:
  datasource:
    order:
      url: jdbc:postgresql://localhost:5432/orderdb
      username: orderuser
      password: orderpassword
      driver-class-name: org.postgresql.Driver
    delivery:
      url: jdbc:mysql://localhost:3306/deliverydb
      username: deliveryuser
      password: deliverypassword
      driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        show_sql: true

๊ทธ๋ฆฌ๊ณ  ๊ฐ๊ฐ DB ์— ๋งž๋Š” DataSourceProperties ๋ฅผ ์ƒ์„ฑํ•˜๋„๋ก @ConfigurationProperties ๋กœ custom properties bean ์„ ์ƒ์„ฑํ•ด์ฃผ์ž.

@Configuration
class DeliveryJpaConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.delivery")
    fun deliveryDataSourceProperties() = DataSourceProperties()

    @Bean
    fun deliveryDataSource(): DataSource = deliveryDataSourceProperties()
        .initializeDataSourceBuilder()
        .build()
}

@Configuration
class OrderJpaConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.order")
    fun orderDataSourceProperties() = DataSourceProperties()

    @Bean
    fun orderDataSource(): DataSource = orderDataSourceProperties()
        .initializeDataSourceBuilder()
        .build()
}

 

์ด๋Ÿฌํ•œ ๋ฐฉ์‹์€ spring boot ์˜ externalized configuration docs ์—์„œ ์ž์„ธํžˆ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

3-2. Jpa EntityManager ์™€ TransactionManager ๋“ฑ๋กํ•˜๊ธฐ

 

์ด์ œ DataSource ๋ฅผ ๊ตฌ์„ฑํ–ˆ์œผ๋‹ˆ JPA ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก Entity Manager ์™€ TransactionManager ๋ฅผ ๋“ฑ๋กํ•ด์ฃผ์ž.

 

๋‚˜๋Š” ์•ž์„œ ๋งŒ๋“ค์—ˆ๋˜ OrderJpaConfig ์™€ DeliveryJpaConfig ํ•˜์œ„์— ๊ฐ๊ฐ ๋งŒ๋“ค์–ด ์ค„ ๊ฒƒ์ด๋‹ค

 

DeliveryJpaConfig.kt

 

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
    basePackageClasses = [DeliveryEntity::class],
    entityManagerFactoryRef = "deliveryEntityManagerFactory",
    transactionManagerRef = "deliveryTransactionManager"
)
class DeliveryJpaConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.delivery")
    fun deliveryDataSourceProperties() = DataSourceProperties()

    @Bean
    fun deliveryDataSource(): DataSource = deliveryDataSourceProperties()
        .initializeDataSourceBuilder()
        .build()

    @Bean
    fun deliveryEntityManagerFactory(
        @Qualifier("deliveryDataSource") dataSource: DataSource?,
    ): LocalContainerEntityManagerFactoryBean {

        val vendorAdapter = HibernateJpaVendorAdapter()
        vendorAdapter.setGenerateDdl(true)

        val factory = LocalContainerEntityManagerFactoryBean()
        factory.dataSource = dataSource
        factory.jpaVendorAdapter = vendorAdapter
        factory.setPackagesToScan("com.example.atomikos.infra.delivery")
        return factory
    }

    @Bean
    fun deliveryTransactionManager(
        @Qualifier("deliveryEntityManagerFactory") entityManagerFactory: EntityManagerFactory
    ): PlatformTransactionManager {
        val txManager = JpaTransactionManager()
        txManager.entityManagerFactory = entityManagerFactory
        return txManager
    }
}

 

OrderJpaConfig.kt

 

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
    basePackageClasses = [OrderEntity::class],
    entityManagerFactoryRef = "orderEntityManagerFactory",
    transactionManagerRef = "orderTransactionManager"
)
class OrderJpaConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.order")
    fun orderDataSourceProperties() = DataSourceProperties()

    @Bean
    fun orderDataSource(): DataSource = orderDataSourceProperties()
        .initializeDataSourceBuilder()
        .build()

    @Bean
    @Primary
    fun orderEntityManagerFactory(
        @Qualifier("orderDataSource") dataSource: DataSource?,
    ): LocalContainerEntityManagerFactoryBean {
        val vendorAdapter = HibernateJpaVendorAdapter()
        vendorAdapter.setGenerateDdl(true)

        val factory = LocalContainerEntityManagerFactoryBean()
        factory.dataSource = dataSource
        factory.setPackagesToScan("com.example.atomikos.infra.order")
        factory.jpaVendorAdapter = vendorAdapter
        return factory
    }

    @Bean
    fun orderTransactionManager(
    	@Qualifier("orderEntityManagerFactory") entityManagerFactory: EntityManagerFactory
    ): PlatformTransactionManager {
        val txManager = JpaTransactionManager()
        txManager.entityManagerFactory = entityManagerFactory
        return txManager
    }
}

 

์—ฌ๊ธฐ์„œ ์ฃผ์˜ํ•ด์•ผ ํ•  ๊ฒƒ์€ OrderJpaConfig ์— @Primary ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์—ˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

 

์ด๋Š” spring data ์—์„œ default properties ๋Š” ๋‹จ์ผ ๋ฐ์ดํ„ฐ์†Œ์Šค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ๋ ‡๋‹ค.

 

์ด์ œ Entity ๋ฐ Repository ๋ฅผ ๋งŒ๋“ค๊ณ  Spring Application ์„ ์‹คํ–‰์‹œ์ผœ๋ณด์ž.

 

@Entity
@Table(name = "orders")
data class OrderEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column(name = "order_name", nullable = false, length = 255)
    val orderName: String,

    @Column(name = "order_date", nullable = false, updatable = false)
    val orderDate: LocalDateTime = LocalDateTime.now()
)

interface OrderRepository: JpaRepository<OrderEntity, Long> {}

@Entity
@Table(name = "deliveries")
data class DeliveryEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column(name = "delivery_name", nullable = false, length = 255)
    val deliveryName: String,

    @Column(name = "delivery_date", nullable = false, updatable = false)
    val deliveryDate: LocalDateTime = LocalDateTime.now()
)

interface DeliveryRepository: JpaRepository<DeliveryEntity, Long> {}

 

๊ทธ๋Ÿผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ •์ƒ์ ์œผ๋กœ 8080 ํฌํŠธ๋ฅผ ๋ฌผ๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹คํ–‰๋˜์—ˆ๋‹ค๊ณ  ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค