1. 개념
Primary-Secondary 구조는 데이터베이스 성능을 최적화하기 위해 읽기(Read)와 쓰기(Write) 작업을 분리하는 아키텍처입니다.
2. 작동 원리
3. 장점
4. 단점
1. 읽기 작업
읽기 요청은 Secondary 노드에서 처리하여 성능을 최적화할 수 있습니다.
@Transactional(readOnly = true)를 활용하여 읽기 전용 트랜잭션을 설정합니다.@Transactional(readOnly = true)
public List<Order> fetchOrders() {
return orderRepository.findAll();
}2. 쓰기 작업
쓰기 요청은 Primary 노드에서 처리하여 데이터의 무결성을 보장해야 합니다.
@Transactional을 활용하여 쓰기 작업이 Primary 노드에서 수행되도록 설정합니다.@Transactional
public void saveOrder(Order order) {
orderRepository.save(order);
}3. Read/Write 분리가 필요한 이유
Primary-Secondary 구조를 활용한 Read/Write 분리는 데이터베이스 성능을 최적화하고 시스템 확장성을 확보할 수 있는 효과적인 방법입니다. 이를 통해 읽기 작업이 많은 환경에서도 데이터 일관성과 성능을 동시에 유지할 수 있습니다. 데이터 동기화 지연과 같은 단점은 애플리케이션 설계 단계에서 적절한 전략을 통해 해결해야 합니다.
Primary 노드는 쓰기 작업을 처리하고, Secondary 노드는 읽기 작업을 처리하도록 데이터 소스를 설정합니다. Spring Boot의 DataSourceConfiguration에 각각의 데이터 소스를 설정합니다.
1. application.yml 설정
spring:
datasource:
primary:
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/spring_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: root
connectionTimeout: 30000
secondary:
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/spring_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: root
connectionTimeout: 30000
spring.datasource.primary와 spring.datasource.secondary로 각각 Primary(쓰기 전용)와 Secondary(읽기 전용) 데이터 소스의 설정을 분리합니다.hikari 블록 내에 데이터 소스 연결에 필요한 필수 속성을 정의합니다.useSSL=false, serverTimezone=UTC, allowPublicKeyRetrieval=true)을 통해 SSL 사용 안함, 타임존 설정, 공개 키 검색 허용 등을 지정합니다.2. DataSourceConfiguration 클래스
@Configuration
public class DataSourceConfiguration {
public static final String PRIMARY_DATASOURCE = "primaryDataSource";
public static final String SECONDARY_DATASOURCE = "secondaryDataSource";
// Primary 데이터 소스 빈 생성 (쓰기 전용)
@Bean(PRIMARY_DATASOURCE)
@ConfigurationProperties(prefix = "spring.datasource.primary.hikari")
public DataSource primaryDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
// Secondary 데이터 소스 빈 생성 (읽기 전용)
@Bean(SECONDARY_DATASOURCE)
@ConfigurationProperties(prefix = "spring.datasource.secondary.hikari")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
// 동적 라우팅 데이터 소스 빈 생성
@Bean
@Primary
@DependsOn({PRIMARY_DATASOURCE, SECONDARY_DATASOURCE})
public DataSource routingDataSource(
@Qualifier(PRIMARY_DATASOURCE) DataSource primaryDataSource,
@Qualifier(SECONDARY_DATASOURCE) DataSource secondaryDataSource) {
// Custom RoutingDataSource 인스턴스 생성
RoutingDataSource routingDataSource = new RoutingDataSource();
// 타깃 데이터소스 매핑: 키("Primary", "Secondary")는 determineCurrentLookupKey() 메서드와 일치해야 함
Map<Object, Object> datasourceMap = new HashMap<>();
datasourceMap.put("primary", primaryDataSource);
datasourceMap.put("secondary", secondaryDataSource);
routingDataSource.setTargetDataSources(datasourceMap);
// 기본 데이터 소스로 Primary를 지정 (트랜잭션이 읽기 전용이 아닐 경우 사용)
routingDataSource.setDefaultTargetDataSource(primaryDataSource);
routingDataSource.afterPropertiesSet(); // 내부 설정값 초기화
return routingDataSource;
}
}
PRIMARY_DATASOURCE와 SECONDARY_DATASOURCE 상수를 통해 각각의 데이터 소스 빈 이름을 정의합니다.@Qualifier로 주입할 때 정확하게 구분할 수 있습니다.@ConfigurationProperties(prefix = "spring.datasource.primary.hikari")와 같이 YAML의 설정값이 해당 빈에 바인딩됩니다.DataSourceBuilder.create().type(HikariDataSource.class).build()를 사용하여 HikariCP 기반의 데이터 소스를 생성합니다.@Primary로 지정하여, 다른 컴포넌트(JPA 등)에서 기본적으로 주입받는 데이터 소스가 되도록 합니다.@DependsOn 어노테이션을 사용하여 Primary와 Secondary 데이터 소스가 먼저 생성된 후 이 빈이 생성되도록 합니다.setTargetDataSources(datasourceMap)를 통해 두 데이터 소스를 키("primary", "secondary")로 매핑합니다.setDefaultTargetDataSource(primaryDataSource)로 기본값을 Primary로 지정합니다.afterPropertiesSet()을 호출하여 내부 설정을 초기화합니다.3. RoutingDataSource 클래스
public class RoutingDataSource extends AbstractRoutingDataSource {
@NotNull
@Override
protected Object determineCurrentLookupKey() {
// 현재 트랜잭션이 읽기 전용이면 "secondary"를, 그렇지 않으면 "primary"를 반환
return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "secondary" : "primary";
}
}
RoutingDataSource는 Spring에서 제공하는 AbstractRoutingDataSource를 상속받아 구현됩니다.TransactionSynchronizationManager.isCurrentTransactionReadOnly()로 확인합니다.전체 동작 흐름
routingDataSource 빈은 Primary와 Secondary 데이터 소스를 매핑하는 Custom RoutingDataSource를 생성합니다.RoutingDataSource의 determineCurrentLookupKey()가 호출되어 현재 트랜잭션의 읽기 전용 여부를 판단합니다.1. Primary에서 쓰기 작업
Primary 노드를 사용하여 주문 데이터를 저장합니다. Spring의 @Transactional을 사용하여 트랜잭션 처리를 추가합니다.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional
public void saveOrder(Order order) {
orderRepository.save(order);
}
}
@Transactional은 기본적으로 읽기와 쓰기 작업을 처리하도록 설정됩니다.2. Secondary에서 읽기 작업
Secondary 노드를 사용하여 주문 데이터를 조회합니다. @Transactional(readOnly = true)를 사용하여 읽기 전용 트랜잭션으로 설정합니다.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional(readOnly = true)
public List<Order> fetchOrders() {
return orderRepository.findAll();
}
}
@Transactional(readOnly = true)를 통해 읽기 작업이 Secondary 노드에서 수행되도록 설정합니다.@Transactional(readOnly = true)를 활용하여 불필요한 쓰기 잠금을 방지합니다.Primary-Secondary 구조와 Read/Write 분리를 활용하여 데이터베이스 성능을 최적화할 수 있습니다. Primary에서 쓰기 작업을 처리하고 Secondary에서 읽기 작업을 분산시켜 시스템 부하를 줄이는 동시에 확장성을 높일 수 있습니다. 성능 테스트를 통해 Primary-Secondary 구조의 장점과 데이터 동기화 지연과 같은 단점을 확인하고, 이를 보완할 전략을 설계할 필요가 있습니다.