스프링 부트에서 JPA는 기본 셋팅은 1개의 datasource만 설정하게 되어 있다. 하지만, master / slave replication이 되어 있는 디비를 둘 다 연결하고 싶을 때에는 조금 까다롭게 설정해야 한다.
두 가지 방식 정도로 할 수 있는데, @Transactional방식과 @AOP를 이용한 방식이 있다. 일단 이 글에서는 Transactional방식을 먼저…
build.gradle
plugins {
id 'org.springframework.boot' version '2.1.6.RELEASE'
id 'java'
}
apply plugin: 'io.spring.dependency-management'
group = 'com.mudchobo.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
application.properties
spring.datasource.master.jdbc-url=jdbc:mysql://localhost:3306/master?serverTimezone=Asia/Seoul
spring.datasource.master.username=root
spring.datasource.master.password=
spring.datasource.slave.jdbc-url=jdbc:mysql://localhost:3306/slave?serverTimezone=Asia/Seoul
spring.datasource.slave.username=root
spring.datasource.slave.password=
spring.jpa.database=mysql
spring.jpa.hibernate.use-new-id-generator-mappings=false
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop
slave에 아래 테이블과 데이터를 넣어서 마스터랑 슬레이브를 테스트를 위해 구분하자. 마스터테이블은 create-drop으로 생성.
test.sql
create table hello (
id bigint not null auto_increment,
world varchar(255),
primary key (id)
) engine=MyISAM;
INSERT INTO `slave`.`hello` (`world`) VALUES ('jared.s');
Transactional readOnly에 따라 분기하는 CustomRoutingDataSource
ReplicationRoutingDataSource.java
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
? "slave"
: "master";
}
}
마스터 데이터소스와 슬레이브 데이터소스를 정의하고, 그걸 분기하는걸 만들어놓은 설정이다.
DataSourceConfig.java
@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"com.mudchobo.example.masterslave"})
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
var routingDataSource = new ReplicationRoutingDataSource();
var dataSourceMap = new HashMap<>();
dataSourceMap.put("master", masterDataSource);
dataSourceMap.put("slave", slaveDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
@Primary
@Bean
public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
}
기본 DataSource 설정을 제외하고 커스텀한걸로 @Bean으로 만들어서 바꿈.
Hello.java
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Hello {
@GeneratedValue
@Id
long id;
@Column
String world;
}
HelloRepository.java
public interface HelloRepository extends CrudRepository<Hello, Long> {
}
HelloService.java
@Transactional
@Service
public class HelloService {
private final HelloRepository helloRepository;
public HelloService(HelloRepository helloRepository) {
this.helloRepository = helloRepository;
}
public Optional<Hello> get(long id) {
return helloRepository.findById(id);
}
public Hello save(Hello hello) {
return helloRepository.save(hello);
}
}
슬레이브 서비스에는 readOnly=true를 넣는다.
HelloSlaveService.java
@Transactional(readOnly = true)
@Service
public class HelloSlaveService {
private final HelloRepository helloRepository;
public HelloSlaveService(HelloRepository helloRepository) {
this.helloRepository = helloRepository;
}
public Optional<Hello> get(long id) {
return helloRepository.findById(id);
}
}
샘플로 save 후 helloService에서 가져올 때와 helloSlaveService에서 가져올 때 로그를 찍었다.
MasterSlaveApplication.java
@Slf4j
@SpringBootApplication
public class MasterSlaveApplication {
public static void main(String[] args) {
SpringApplication.run(MasterSlaveApplication.class, args);
}
@Bean
public CommandLineRunner demo(HelloService helloService, HelloSlaveService helloSlaveService) {
return args -> {
var savedHello = helloService.save(Hello.builder().world("mudchobo").build());
log.info("savedHello = {}", savedHello);
var hello1 = helloService.get(1);
log.info("hello1 = {}", hello1);
var hello2 = helloSlaveService.get(1);
log.info("hello2 = {}", hello2);
};
}
}
결과
savedHello = Hello(id=1, world=mudchobo)
hello1 = Optional[Hello(id=1, world=mudchobo)]
hello2 = Optional[Hello(id=1, world=jared.s)]
이 방식의 단점은 쓸데없이 @Transactional을 붙여야 한다. 이걸 붙이지 않으면, findById만 하는 곳에서는 readOnly로 취급해서 무조건 slave를 가져오게 된다. 그래서 AOP방식으로 쓰는 게 더 나을 것 같다.