Spring DataSource Replication 구성하기
Configure DataSource Replication in Spring
현대의 웹 애플리케이션은 고가용성과 지속적인 확장성을 요구합니다. 이 두 가지 요소를 효과적으로 달성하는 방법 중 하나는 읽기와 쓰기 작업이 수행되는 데이터베이스를 별도로 구성하여 데이터베이스 복제 환경을 만드는 것입니다. 이런 환경에서, 읽기 요청은 하나의 데이터베이스에서 처리되고 쓰기 요청은 다른 데이터베이스에서 처리되므로, 부하 분산과 성능 향상을 실현할 수 있습니다.
이 글에서는 이미 구축된 Database Replication 환경을 JPA 기반의 Spring 프로젝트에 어떻게 적용할 수 있는지, 그 과정을 함께 살펴보도록 하겠습니다.
Prerequisite
본 프로젝트는 다음과 같은 환경을 기반으로 구성되어 있습니다:
- Java 17
- Spring Boot 3.1.4
- JPA
- Gradle 8
- Database: AWS Aurora RDS
Setting Up DataSource Replication
AWS Aurora RDS는 클러스터 기반의 구조로 되어 있으며, 쓰기 작업과 읽기 작업을 분리할 수 있는 전용 클러스터 엔드포인트를 제공합니다.
이러한 기능을 활용해 Spring Boot 프로젝트에서 쓰기 전용 데이터베이스와 읽기 전용 데이터베이스를 어떻게 설정하는지 알아보겠습니다. 이 설정 과정을 통해 데이터베이스 읽기 및 쓰기 작업을 효율적으로 분산시키고, 그 결과로 애플리케이션의 비즈니스 로직 처리에 더 높은 성능과 확장성을 제공할 수 있게 됩니다.
Defining Properties
데이터 소스의 복제 설정을 위해 가장 먼저 해야 할 일은 application.yml
파일에 데이터베이스 접속 정보를 정의하는 것입니다. Writer와 Reader에 대한 각각의 Endpoint, 사용자 이름 및 패스워드 정보가 필요합니다. 이 정보는 AWS Console에서 확인할 수 있습니다.
spring:
datasource:
writer:
jdbc-url: jdbc:mysql://{WRITER_ENDPOINT}
username: {USERNAME_HERE}
password: {PASSWORD_HERE}
reader:
jdbc-url: jdbc:mysql://{READER_ENDPOINT}
username: {USERNAME_HERE}
password: {PASSWORD_HERE}
Creating DataSource Beans
다음은, 데이터베이스 연결에 필요한 정보를 담고 있는 DataSource
객체를 만들고 이를 Spring Bean으로 등록합니다. 쓰기 전용과 읽기 전용에 대한 각각의 Bean을 만들어야 합니다.
@Slf4j
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.writer")
public DataSource writerDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.reader")
public DataSource readerDataSource() {
return DataSourceBuilder.create().build();
}
}
@ConfigurationProperties
:application.yml
구성 파일 내부의 값을 바인딩합니다.prefix
를 통해 특정 속성을 명시할 수 있습니다. 여기서는, 앞서 정의한 writer와 reader 데이터베이스 접속 정보(url, username, password)를 가져옵니다.
- DataSourceBuilder:
@ConfigurationProperties
어노테이션을 통해 사전에 구성한 설정 값들을 바탕으로DataSource
객체를 초기화합니다.
이와 같이 읽기 및 쓰기 작업 별로 각각의 DataSource
객체를 세팅함으로써, 데이터베이스 연결의 세부적인 설정을 보다 유연하게 관리할 수 있게 됩니다.
Dynamic DataSource Routing
다음은, Spring에서 제공하는 AbstractRoutingDataSource
를 확장하여, 트랜잭션 모드를 구분할 수 있는 값을 제공합니다.
public class DataSourceRoutingConfig extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
? "reader"
: "writer";
}
}
determineCurrentLookupKey()
: 현재 트랜잭션이 읽기용인지 아닌지를 체크하여 그에 맞는DataSource
의 키를 반환합니다. 이 값은 이후에Map<key, value>
객체의 키 역할을 하게 됩니다.@Transactional(readOnly = true)
와 같이 읽기 전용 트랜잭션인 경우,reader
문자열을 반환합니다.@Transactional(readOnly = false)
와 같이 쓰기 가능한 트랜잭션인 경우,writer
문자열을 반환합니다.
위에서 확장한 DataSourceRoutingConfig
객체를 활용하여, 트랜잭션 모드에 따라 동적으로 DataSource
를 할당하는 Spring Bean을 추가합니다.
@Slf4j
@Configuration
public class DataSourceConfig {
...
@Bean
public DataSource routingDataSource(
@Qualifier("writerDataSource") DataSource writerDataSource,
@Qualifier("readerDataSource") DataSource readerDataSource) {
DataSourceRoutingConfig routingDataSource = new DataSourceRoutingConfig();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("writer", writerDataSource);
dataSourceMap.put("reader", readerDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(writerDataSource);
return routingDataSource;
}
@Bean
@Primary
public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
}
routingDataSource()
: Spring Bean으로 등록될DataSource
를 동적 라우팅하는 역할을 합니다.setTargetDataSources()
: 애플리케이션에서 사용할DataSource
목록을Map<key, value>
형태로 등록하여 관리합니다. 여기서,Map
의 key는 앞서AbstractRoutingDataSource
를 확장한DataSourceRoutingConfig
에서 명시적으로 설정하였습니다.setDefaultTargetDataSource()
: 기본DataSource
로는writer
데이터베이스를 설정합니다.
@Primary
: Spring에 동일한 Bean이 여러개 존재하는 경우, 이 어노테이션이 선언된 Bean이 우선권을 지닙니다.LazyConnectionDataSourceProxy
:routingDataSource
를 프록시로 가지는DataSource
를 생성합니다. 이 구조를 통해 데이터베이스 연결이 필요한 시점에 자동으로 연결이 이루어지는 지연 연결 방식을 구현해 성능 이점을 얻을 수 있습니다.
Playgrounds
읽기 및 쓰기 전용 데이터베이스를 동적으로 할당하기 위해 필요한 작업을 완료하였습니다. 테스트를 통해 트랜잭션 모드에 따라 적절한 DataSource
를 가져오는지 확인해 보겠습니다.
먼저, application.yml
에 데이터베이스 연결 정보를 추가합니다. 구분을 위해 로컬 머신에 두 개의 데이터베이스를 생성하였습니다:
spring:
datasource:
writer:
jdbc-url: jdbc:mysql://localhost:3306/writer
username: catsriding
password: 1234
reader:
jdbc-url: jdbc:mysql://localhost:3306/reader
username: catsriding
password: 1234
그리고 아래와 같이 서비스 객체를 구현하였습니다:
@Slf4j
@Service
@RequiredArgsConstructor
public class WavesService {
private final DataSource dataSource;
@Transactional(readOnly = false)
public void write() {
logDataSourceInfo();
}
@Transactional(readOnly = true)
public void read() {
logDataSourceInfo();
}
private void logDataSourceInfo() {
try {
Connection connection = dataSource.getConnection();
DatabaseMetaData metaData = connection.getMetaData();
log.info("Url: " + metaData.getURL());
log.info("isReadOnly: {}", TransactionSynchronizationManager.isCurrentTransactionReadOnly());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
write()
:@Transactional(readOnly = false)
를 통해 쓰기가 가능한 트랜잭션을 시작합니다.read()
:@Transactional(readOnly = true)
를 통해 읽기 전용 트랜잭션을 시작합니다.logDataSourceInfo()
: 할당된DataSource
정보와 트랜잭션 정보를 로그에 출력합니다.Connection
: 데이터베이스 연결 정보를 지니고 있는 객체입니다.TransactionSynchronizationManager
: 현재 스레드에 바인딩된 실제 트랜잭션의 정보를 담고 있으며,isCurrentTransactionReadOnly()
를 통해readOnly
속성을 반환합니다.
이제 테스트 코드를 작성하여 위에서 구현한 write()
함수를 호출하고 그 결과를 확인해 보겠습니다:
@SpringBootTest
@ActiveProfiles("test")
public class DataSourceRoutingTest {
@Autowired
WavesService wavesService;
@Test
public void testRouting() {
wavesService.write();
}
}
콘솔 로그를 통해, writer 데이터베이스가 할당되었으며 트랜잭션 역시 쓰기가 가능한 모드인 것을 확인할 수 있습니다.
Url: jdbc:mysql://localhost:3306/writer
isReadOnly: false
다음은 read()
함수를 호출해 보겠습니다:
@SpringBootTest
@ActiveProfiles("test")
public class DataSourceRoutingTest {
@Autowired
WavesService wavesService;
@Test
public void testRouting() {
- wavesService.write();
+ wavesService.read();
}
}
예상한 대로, reader 데이터베이스가 할당되고 읽기 전용 트랜잭션으로 시작된 것을 확인할 수 있습니다.
Url: jdbc:mysql://localhost:3306/reader
isReadOnly: true
여기까지, Database Replication 전략을 적용하기 위한 읽기 및 쓰기 전용 DataSource
Bean을 등록하고, 트랜잭션 모드에 따라 동적으로 할당하는 환경을 구성해 보았습니다.
- Spring
- JPA