Multi Datasource Application ์ธํ ํ๊ธฐ (with Spring Boot 3.3 + JPA + kotlin)
๋ชฉ์ฐจ
- Multi DataSource Application ํ๊ฒฝ
- ์ค์ต ํ๊ฒฝ ๊ตฌ์ฑ
- Spring Application ๊ตฌ์ฑ - 1. DataSource ๊ตฌ์ฑ
- Spring Application ๊ตฌ์ฑ - 2. EntityMangager TxManager ๊ตฌ์ฑ
1. Multi DataSource Application ํ๊ฒฝ
Spring Application ์ ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ๋ถ๋ช ํ๋์ applcation ์์ ๋๊ฐ ์ด์์ DB ๋ฅผ ๋ฐ๋ผ๋ด์ผ ํ๋ ๊ฒฝ์ฐ๊ฐ ์กด์ฌํ๋ค.
์ด๋ฒ ์ค์ต์์๋ ํ๋์ 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 ํฌํธ๋ฅผ ๋ฌผ๊ณ ์ ํ๋ฆฌ์ผ์ด์ ์ด ์คํ๋์๋ค๊ณ ํ์ธํ ์ ์๋ค