뭘 빼야하는지 확인하기 귀찮아서 그냥 현재 프로젝트 구성중인 설정을 다 넣었습니다.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
buildscript {
val kotlinVersion = "1.6.0"
dependencies {
classpath("gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:1.0.10")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}
}
repositories {
mavenCentral()
maven("https://plugins.gradle.org/m2/")
}
plugins {
id("org.springframework.boot") version "3.0.5"
id("io.spring.dependency-management") version "1.1.0"
kotlin("jvm") version "1.6.0"
kotlin("plugin.spring") version "1.6.0"
kotlin("plugin.jpa") version "1.6.0"
kotlin("kapt") version "1.7.10"
}
group = "com.ldg"
version = "0.0.1-SNAPSHOT"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-mustache")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation("com.querydsl:querydsl-mongodb")
implementation("org.mongodb:mongodb-driver-sync")
implementation("org.mongodb:bson")
implementation("org.mongodb:mongo-java-driver")
kapt("org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final")
compileOnly ("org.projectlombok:lombok")
annotationProcessor ("org.springframework.boot:spring-boot-configuration-processor:3.0.5")
annotationProcessor ("org.projectlombok:lombok")
implementation("io.jsonwebtoken:jjwt:0.9.1")
implementation("com.auth0:java-jwt:3.18.1")
//maria db
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
// query dsl
val querydslVersion = "5.0.0"
implementation("com.querydsl:querydsl-jpa:$querydslVersion")
kapt("com.querydsl:querydsl-apt:$querydslVersion:jakarta")
annotationProcessor(group = "com.querydsl", name = "querydsl-apt", classifier = "jakarta")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
//runtimeOnly("com.h2database:h2")
runtimeOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group="junit", module="unit")
}
testImplementation("org.junit.jupiter:junit-jupiter-api:5.5.2")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.5.2")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.5.2")
testImplementation("org.springframework.security:spring-security-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = JavaVersion.VERSION_17.toString()
}
java.sourceCompatibility = JavaVersion.VERSION_17
}
tasks.named("compileJava") {
inputs.files(tasks.named("processResources"))
}
tasks.withType<Test> {
useJUnitPlatform()
}
server:
port: 8080
spring:
jpa:
show-sql: true
hibernate:
ddl-auto: create
generate-ddl: true
main:
allow-bean-definition-overriding: true
primary:
datasource:
driver-class-name: org.mariadb.jdbc.Driver
jdbc-url: jdbc:mariadb://127.0.0.1:13306/prime?useUnicode=true&characterEncoding=utf8
username: root
password: root
secondary:
datasource:
driver-class-name: org.mariadb.jdbc.Driver
jdbc-url: jdbc:mariadb://127.0.0.1:23306/prime?readOnly=true&useUnicode=true&characterEncoding=utf8
username: root
password: root
logging:
level:
org:
springframework:
data:
mongodb:
core:
MongoTemplate: DEBUG
package com.ldg.prime.config
import org.slf4j.LoggerFactory
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.jdbc.DataSourceBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter
import org.springframework.transaction.annotation.EnableTransactionManagement
import org.springframework.transaction.support.TransactionSynchronizationManager
import java.util.*
import javax.sql.DataSource
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "primaryEntityManagerFactory",
basePackages = ["com.ldg.prime.maria.master.repository"] //지정한 경로의 repository들은 모두 단방향 이중화 설정을 적용
)
class JpaConfig {
@Bean(name = ["writeDataSource"])
@ConfigurationProperties(prefix = "primary.datasource")
fun writeDataSource(): DataSource {
return DataSourceBuilder.create().build()
}
@Bean(name = ["readDataSource"])
@ConfigurationProperties(prefix = "secondary.datasource")
fun readDataSource(): DataSource {
return DataSourceBuilder.create().build()
}
@Primary
@Bean
fun routingDataSource(): DataSource {
val routingDataSource = RoutingDataSource()
val readDataSourceProxy = LazyConnectionDataSourceProxy(readDataSource())
val writeDataSourceProxy = LazyConnectionDataSourceProxy(writeDataSource())
routingDataSource.setTargetDataSources(mapOf("readDataSource" to readDataSourceProxy, "writeDataSource" to writeDataSourceProxy))
routingDataSource.setDefaultTargetDataSource(readDataSourceProxy) // 기본 data source는 slave로 설정
return routingDataSource
}
@Primary
@Bean(name = ["primaryEntityManagerFactory"])
fun entityManagerFactory(): LocalContainerEntityManagerFactoryBean {
val entityManagerFactory = LocalContainerEntityManagerFactoryBean()
entityManagerFactory.dataSource = routingDataSource()
entityManagerFactory.setPackagesToScan("com.ldg.prime.maria.master.entity")
entityManagerFactory.jpaVendorAdapter = HibernateJpaVendorAdapter()
entityManagerFactory.setJpaProperties(hibernateProperties())
entityManagerFactory.persistenceUnitName = "prime"
return entityManagerFactory
}
private fun hibernateProperties(): Properties {
val properties = Properties()
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MariaDBDialect")
properties.setProperty("hibernate.show_sql", "true")
properties.setProperty("hibernate.format_sql", "true")
return properties
}
class RoutingDataSource : AbstractRoutingDataSource() {
private val log = LoggerFactory.getLogger(javaClass)
public override fun determineCurrentLookupKey(): Any {
log.error("{} determineCurrentLookupKey isCurrentTransactionReadOnly", TransactionSynchronizationManager.isCurrentTransactionReadOnly().toString())
return if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) "readDataSource" else "writeDataSource"
}
}
}
만약 위 설정에서 transactionManager를 설정을 추가하면 @Transctional의 설정을 가져오기 전에 이미 data source를 가져와버리는 현상이 있어 readOnly 설정을 읽지 못하는 경우가 발생합니다. JPA transactionManager의 기본 옵션인지 오류인지는 모르겠습니다.
[2023.04.11 JpaConfig.kt 추가사항]
@Bean(name = ["masterTransactionManager"])
fun masterTransactionManager(@Qualifier("masterEntityManagerFactory") emFactory: EntityManagerFactory?) : JpaTransactionManager{
val transactionManager = JpaTransactionManager()
transactionManager.entityManagerFactory = emFactory
return transactionManager
}
@Primary
@Bean(name = ["slaveTransactionManager"])
fun slaveTransactionManager(): PlatformTransactionManager {
return DataSourceTransactionManager(routingDataSource())
}
일단 기본적으로 BeanName을 primary, second 이런식으로 했는데 목적에 맞게 master, slave로 변경하였습니다. 그리고 transactionManager가 없으니 JPA에서 save 할 때 insert나 update가 이뤄지지 않았습니다. 그래서 readOnly 설정을 읽을 수 있는 transactionManager가 무엇일까 찾아보던 도중 DataSourceTransactionManager를 적용하면 정상적으로 작동하는 것을 확인 하였습니다.
[예시]
@Transactional(readOnly = true, transactionManager = "slaveTransactionManager")
@Transactional(transactionManager = "masterTransactionManager")
위 예시 처럼 @Transactional의 transactionManager 속성으로 설정한 매니저를 지정해주면 됩니다. 다만, read 할때 write할때 다르게 지정해줘야합니다. 아래 이미지는 실제 코드 적용 사례입니다. transactionManager 이름은 String 값이라 전역 상수로 관리도 가능합니다.