데이터베이스 다중화란 보통 서버 사이에주(Master) 부(Slave) 관계를 설정하고 데이터 원본은 Master, 사본은 Slave 서버에 저장하는 방식이다.
여기서 데이터 베이스 Insert, delete, update 쿼리들 (쓰기 연산)은 Master 서버에서, select Query는 Slave 서버에서 처리한다.
대부분 애플리케이션은 읽기 연산의 비중이 쓰기 연산 비중보다 높다. 그러므로 보통 Slave 서버가 Master서버 보다 많이 존재한다.
DB 다중화의 이점으로는 크게 3가지가 존재한다.
1. 더 나은 성능 주-부 다중화 모델에서 읽기, 쓰기 연산을 분산 처리하기 때문에 처리할 수 있는 Query수가 많아져 더 나은 성능을 보장한다.
2. 안정성 : DB서버의 장애가 발생해도 다른 DB 서버의 데이터를 보존할 수 있다.
3. 가용성 : DB서버의 장애가 발생해도 다른 서버의 데이터로 서비스할 수 있다.
이번 글에서는 Master, Slave 관계를 설정하고 만드는 과정을 Spring Boot로 진행해 보면서 자세히 알아볼 예정이다.
Spring Boot에서 yml, properties로 DB의 메타정보를 등록하면 알아서 DB관리를 해주었다.
그렇다면 직접 Master, Slave에 연결해줄 때는 어떻게 해야 할까? 그전에 Spring에서 DB를 어떻게 직접 구성해주는지 알아보자
DB Connection Pool
데이터 연동시 웹 애플리케이션은 필요할 때마다 DB에 연결하여 데이터를 주고받는다.
하지만 만약 요청이 올 때마다 DB와 연결을 끊고 재연 결한다면 오버헤드가 상당히 많이 발생한다.
그렇기 때문에 DB와 연결을 미리 설정해 두고 필요에 따라 Connection을 사용하고 반환하여 빠른 연동 작업을 가능하게 한다.
결국 DB와 미리 연결해둔 객체를 Pool에 저장해 두고 요청이 오면 Connection을 빌려주고 끝나면 다시 반납받아 Pool에 저장하는 방식이다.
Java에서 DB Connection은 어떻게 이루어질까?
여기서 JDBC API는 DB 접근을 위해 Connection Object를 사용한다.
DataSource란?
사용자 요청마다 Connection을 사용하기 때문에 요청마다 Connection을 연결하고 끊는 오버헤드를 방지하고자 DB 연결 정보 저장, Connection 생성, Connection Pool에 등록하고 관리하는 것이 DataSource이다.
이 글에서는 Read/Write 서버를 Transaction(readOnly)로 나눠서 라우팅 할 수 있도록 한다.
이제 DataSource에 대해 알았으니 DataSource를 이용해 Master/Slave 서버의 Connection을 관리해 보자
YML 설정하기 (Master/Slave)
datasource:
master:
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://{Write_DB_Name}:3306/just?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=UTF-8
username: {username}
password: {password}
read-only: false
slave:
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://{Read_DB_Name}3306/just?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=UTF-8
username: {username}
password: {password}
read-only: true
Enum 설정하기
public enum DataSourceType {
MASTER,
SLAVE;
}
DataSource에 필요한 메타데이터들을 불러오기
Master_prefix 등은 yml에 있는 데이터들의 경로를 설정해 준다.
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class DbConstUtil {
public static final String PROFILE_PROD = "prod";
public static final String BASE_PACKAGES = "com.example.just";
public static final String MASTER_DATE_SOURCE = "masterDataSource";
public static final String SLAVE_DATE_SOURCE = "slaveDataSource";
public static final String MASTER_PREFIX = "spring.datasource.master.hikari";
public static final String SLAVE_PREFIX = "spring.datasource.slave.hikari";
public static final String ROUTING_DATA_SOURCE = "routingDataSource";
public static final String DATA_SOURCE = "dataSource";
public static final String ENTITY_MANAGER_FACTORY = "entityManagerFactory";
public static final String TRANSACTION_MANAGER = "transactionManager";
public static final String ENTITY_MANAGER = "entityManager";
public static final String HIBERNATE_DIALECT = "org.hibernate.dialect.MySQL5Dialect";
}
DataSourceConfig
@Configuration
public class DataSourceConfig { // DataSourceBean생성 Config
@Primary // 우선적 주입
@Bean(MASTER_DATE_SOURCE) // Bean 등록
@ConfigurationProperties(prefix = MASTER_PREFIX) // MASTER_PREFIX로 된 yml 설정 읽어오기
public DataSource masterDataSource() {
return DataSourceBuilder // Spring DataSource 생성 클래스
.create()
.type(HikariDataSource.class)
.build();
}
@Bean(SLAVE_DATE_SOURCE)
@ConfigurationProperties(prefix = SLAVE_PREFIX) // SLAVE_PREFIX로 된 yml 설정 읽어오기
public DataSource replicaDataSource() {
return DataSourceBuilder
.create()
.type(HikariDataSource.class)
.build();
}
}
DataSource Bean으로 등록해 관리해 준다. Master/Slave Bean으로 만들어 관리하자
RoutingDataSourceConfig를 통해 JPA와 동적으로 DataSource를 라우팅 하자.
EnableJpaRepositories( // JAP Repository 활성화 어노테이션
basePackages = BASE_PACKAGES,
entityManagerFactoryRef = ENTITY_MANAGER_FACTORY, // EntityMangeFactory빈 설정
transactionManagerRef = TRANSACTION_MANAGER // Tansaction 관리자 빈 설정
)
@Configuration
public class RoutingDataSourceConfig {
@Bean(ROUTING_DATA_SOURCE)
public DataSource routingDataSource( // 라우티할 데이터베이스 동적 설정
@Qualifier(MASTER_DATE_SOURCE) final DataSource master,
@Qualifier(SLAVE_DATE_SOURCE) final DataSource slave
) {
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceType.MASTER, master);
dataSourceMap.put(DataSourceType.SLAVE, slave);
routingDataSource.setTargetDataSources(dataSourceMap); // 마스터, 슬레이브 두개 타겟 설정
routingDataSource.setDefaultTargetDataSource(master); // Defaul 기본 데이터 소스로 마스터 설정
return routingDataSource;
}
@Bean(DATA_SOURCE)
public DataSource dataSource(@Qualifier(ROUTING_DATA_SOURCE) DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
// 지연 로딩 지원, DB사용시까지
}
@Bean(ENTITY_MANAGER_FACTORY)
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean( // JPA entityMange 생성
@Qualifier(DATA_SOURCE) DataSource dataSource) {
LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
entityManagerFactory.setDataSource(dataSource); // 앞서 받은 datSource 설정
entityManagerFactory.setPackagesToScan(BASE_PACKAGES); // 스캔할 패키지
entityManagerFactory.setJpaVendorAdapter(this.jpaVendorAdapter());
entityManagerFactory.setPersistenceUnitName(ENTITY_MANAGER); // EntityManger Name
// Hibernate 속성을 설정하기 위해 별도의 Properties 객체를 사용합니다.
entityManagerFactory.setJpaProperties(hibernateProperties());
return entityManagerFactory;
}
private Properties hibernateProperties() { // Hibernate 관련 설정
Properties properties = new Properties();
properties.setProperty("hibernate.hbm2ddl.auto", "create");
properties.setProperty("hibernate.format_sql", "true");
return properties;
}
private JpaVendorAdapter jpaVendorAdapter() { // Hibernate 구현체 설정 어댑터 반환
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setGenerateDdl(false);
adapter.setShowSql(false);
adapter.setDatabasePlatform(HIBERNATE_DIALECT);
return adapter;
}
@Bean(TRANSACTION_MANAGER)
public PlatformTransactionManager platformTransactionManager( // JPA Transaction 관리 위환 빈 생성
@Qualifier(ENTITY_MANAGER_FACTORY) LocalContainerEntityManagerFactoryBean emf
) {
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
jpaTransactionManager.setEntityManagerFactory(emf.getObject());
return jpaTransactionManager;
}
}
위 코드에서 JPA Adapater, Hibernate Properites, EntityMangerFactoryBean 등은 익숙해 무엇인지 알 수 있지만
LazyConnectinDataSourceProxy는 무엇인지 잘 모를 수 있다.
Spring은 Transaction 진입 순간 DB Connection을 DataSource에서 가져온다.
하지만 Transcation을 시작하고 외부 서비스를 이용하는 기간 (DB 접근 필요 없는 시간) 등에 Connection을 점유하는 오버헤드 또는 Hibernate의 영속성 콘텍스트에 1차 캐시를 사용해도 되는 환경에서 실제 DB 접근은 필요 없는 경우의 오버헤드가 발생할 수 있다.
그렇기 때문에 해결 방법으로 우리는 LazyConnectionDataSourcePrxoy를 사용했다.
LazyConnectionDataSourcePrxoy란 실제로 Connection이 필요한 순간이 아니라면 Connection을 점유하지 않고 필요한 시점에만 Connection을 점유할 수 있도록 해준다.
결국 LazyConnectionDataSourcePrxoy를 통한 트랜잭션 지연 로딩으로 필요한 순간에만 사용함으로써 불필요한 오버헤드를 피할 수 있다.
AbstractRoutingDataSource를 확장해서 DataSourceRouting을 동적으로 구현하자
읽기/쓰기 작업에 따라 Master/Slave를 자동으로 선택하도록 동작시킨다.
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
// 현재 트랜잭션이 읽기 전용이라면 Slave 데이터 소스를 반환
return DataSourceType.SLAVE;
}
return DataSourceType.MASTER; // 쓰기 전용이라면 Master 데이터 소스 반환
}
}
이제 @Transactional(readOnly = true)를 사용하면 읽기 전용 Slave DataSource를 사용하고 쓰기 전용이라면 Master DataSource를 사용할 수 있게 되었다.
'스프링' 카테고리의 다른 글
[Spring] Github OAuth 2.0 + Jwt를 통해 로그인하기 (3) | 2023.05.18 |
---|---|
[Spring] TDD vs BDD 무엇인지알고 비교하기 (0) | 2023.03.12 |
[Spring] Slice를 이용하여 무한스크롤 구현하기 (0) | 2023.02.12 |
[Spring] Spring Security이용한 JWT 로그인 구현기 (1) | 2023.01.28 |
[Spring] JWT Refresh Token 어디에 저장해야 할까? 그리고 꼭 저장해야 할까? (0) | 2023.01.25 |