Java & kotlin 기반으로 Spring 을 개발하시는 분들은 너무나 익숙한 그림일텐데요
오늘은 Application과 JPA 단에 대해서 깊이 알아보는 시간보다는
DataSource를 통해서 어떻게 DataBase와 통신하는지에 대해 알아보기 위한 게시글입니다 ㅎㅎ
2021.06.19 기준으로 spring-jdbc 제일 최신 버젼인 5.3.8 을 기반으로 알아보도록 하겠습니다
DataSource란?
먼저 우리는 DataSource가 무엇인지 알아야됩니다
DataSource가 무엇일까요?
개발을 조금 해보았거나, 가벼운 토이 프로젝트로 DataBase와 연결하는 작업을 진행할 때
아래와 같은 설정을 해보신 경험이 있으실 겁니다
바로 Application <-> DataBase 간의 Connection을 맺어주고 ConnectionPool을 생성하여
CRUD 가 가능한 작업들을 통신하게 해주는 역할 이라고 볼 수 있습니다
하지만 DataSource 에 대한 연결은
누가 담당하는지 어떻게 이루어지는지 자원을 어떻게 활용하는지에 대해서 정리된 글은 없어서
해당 내용에 대해 알아보는 시간을 가져볼려고 합니다
DataSoure 너는 도대체 누구냐
가볍게 인터페이스를 살펴보도록 할까요?
package javax.sql;
import java.sql.Connection;
import java.sql.ConnectionBuilder;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Wrapper;
/**
* <p>A factory for connections to the physical data source that this
* {@code DataSource} object represents. An alternative to the
* {@code DriverManager} facility, a {@code DataSource} object
* is the preferred means of getting a connection. An object that implements
* the {@code DataSource} interface will typically be
* registered with a naming service based on the
* Java™ Naming and Directory (JNDI) API.
* <P>
* The {@code DataSource} interface is implemented by a driver vendor.
* There are three types of implementations:
* <OL>
* <LI>Basic implementation -- produces a standard {@code Connection}
* object
* <LI>Connection pooling implementation -- produces a {@code Connection}
* object that will automatically participate in connection pooling. This
* implementation works with a middle-tier connection pooling manager.
* <LI>Distributed transaction implementation -- produces a
* {@code Connection} object that may be used for distributed
* transactions and almost always participates in connection pooling.
* This implementation works with a middle-tier
* transaction manager and almost always with a connection
* pooling manager.
* </OL>
* <P>
* A {@code DataSource} object has properties that can be modified
* when necessary. For example, if the data source is moved to a different
* server, the property for the server can be changed. The benefit is that
* because the data source's properties can be changed, any code accessing
* that data source does not need to be changed.
* <P>
* A driver that is accessed via a {@code DataSource} object does not
* register itself with the {@code DriverManager}. Rather, a
* {@code DataSource} object is retrieved through a lookup operation
* and then used to create a {@code Connection} object. With a basic
* implementation, the connection obtained through a {@code DataSource}
* object is identical to a connection obtained through the
* {@code DriverManager} facility.
* <p>
* An implementation of {@code DataSource} must include a public no-arg
* constructor.
*
* @since 1.4
*/
public interface DataSource extends CommonDataSource, Wrapper {
}
아 너무 어렵습니다
영어는 다시 봐도 너무 어렵습니다
그래서 번역해드릴게요 ㅎㅎ
설명에 대해서 쉽게 요약드리자면, datasource 는 데이터 연결을 위한 Factory 라는 것을 나타내고 있습니다.
Connection 객체에 대한 생성을 담당해주는 역할이라는 것을 java Doc 문서에서 너무나도 잘 나타내주고 있습니다.
그렇다면 이런 생각이 들죠
연결을 맺어주기만 하는 역할이라면, 관리는 누가 하는것인가?
우리는 이미 알고 있습니다. 대표적인 것이 바로 HikariCP 이죠 ㅎㅎ
놀랍게도 각각의 역할에 대한 책임을 명확하게 나누었습니다
어쨌든 우리는 오늘은 Connection Pool 에 대해 파헤치는 시간은 아니고,
DataSource 그 자체에 대해 파헤쳐보는 시간이니 HikariCP 에 대한 설명은 생략도록 하겠습니다
( 참고로 Hikari는 DataSource 를 상속한 HikariDataSource 가 별도로 존재합니다 )
아 그러면 정리가 되었습니다
DataSource는
- DB와의 연결을 위한 Factory의 역할로, Connection을 맺어주는 역할이다
- Connection 객체를 생성하면 관리는 Connection Manager에게 위임한다
- Transaction 관리자와 함께 활용되어 Transaction들을 처리한다
라고 볼 수 있습니다
그렇다면 DataSource를 활용하는 전략에는 어떤 것이 있을까요?
분산 환경 데이터베이스
일반적으로 우리가 Application을 띄워놓고 Database에 접근해서 데이터 수정 및 조회에 대한 처리를
다양한 진입점을 통해서 한다면, 이는 분산 환경 데이터베이스를 활용한다 라고 합니다
다양한 진입점을 통해서 하나의 데이터베이스에 접근한다면,
데이터베이스에 대한 부하가 점점 늘어나는 구조가 될 수 밖에 없겠죠?
일반적으로 DataBase 를 활용하는 방법은 여러가지가 있겠지만,
MySql 의 Replication(복제) 정책을 활용한다고 가정하고 말씀드려 보겠습니다
조회(Read), 프로세싱(Create, Update, Delete) 에 대한 연산들이 하나의 데이터베이스로
집중되게 된다면... 어느 순간 부하가 넘쳐나서 데이터베이스 장애 혹은 성능 저하가 일어날 수 있습니다
그에 대한 해결책은 바로 Transaction을 분리하는 것입니다
아까 저희가 DataSource에 대해 정리하면서, DataSource의 큰 역할중에 하나는
DB와의 연결을 맺어주고, DataSource Manager, Transaction Manager 에게 위임하여 연결과 트랜잭션을 처리한다
라고 정리가 되었었죠?
그래서 전략을 잘 짜서 활용한다면 DataSource를 다양하게 맺어보고 Routing 하는 전략을 통해
부하에 대한 분산을 실시할 수 있게 됩니다
Routing Data Source
놀랍게도 Spring 에서 이미 제시해주고 있습니다
package org.springframework.jdbc.datasource.lookup;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
* calls to one of various target DataSources based on a lookup key. The latter is usually
* (but not necessarily) determined through some thread-bound transaction context.
*
* @author Juergen Hoeller
* @since 2.0.1
* @see #setTargetDataSources
* @see #setDefaultTargetDataSource
* @see #determineCurrentLookupKey()
*/
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
}
DataSource에 대한 Routing 정책을 결정하는 클래스입니다
내부 필드를 보시면, targetDataSource 필드가 있는데요.
주로 <String, Datasource> 로 활용하여 어떠한 key 값으로 Datasource를 선택할 것인지 결정하게 됩니다.
그럼 key 전략을 어떠한 로직으로 흐르게 될까요.?
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
@Nullable
protected abstract Object determineCurrentLookupKey();
바로 LoockUpkey를 통해서 어떠한 Datasource를 가져올 것인지 결정하는 것이죠
- LookUpKey를 가져와 본다
- LookUpKey에 해당하는 dataSource가 있다면, 해당 Datasource를 선택하여 가져온다
- 만일 존재하지 않는다면, defaultDataSource를 사용한다
- default조차도 없다면 예외가 발생한다
아하, default를 지정하고, routing 전략을 사용한다면 Datasource를 선택하여 사용할 수 있게 되는 것입니다
그럼 바로 코드를 만들러 가볼까요.?
구현
이론적인 배경들이 완성되었으니 실제로 구현하는 것은 크게 어렵지 않습니다.
먼저 위에서 언급한 DataSource 선택 전략을 구현합니다
internal class RoutingDataSource : AbstractRoutingDataSource() {
override fun determineCurrentLookupKey(): Any = when {
TransactionSynchronizationManager.isCurrentTransactionReadOnly() -> "slave"
else -> "master"
}
}
현재 Transaction이 Read Only이면 "slave" 로
그 외면 "master"로 routing 하는 전략을 짜는 것입니다
전략이 완성되었으니 dataSource를 완성해볼까요.?
@Bean(name = ["masterDataSource"])
@ConfigurationProperties(prefix = "spring.datasource.master")
fun masterDataSource(): DataSource = DataSourceBuilder.create()
.type(HikariDataSource::class.java)
.build()
@Bean(name = ["slaveDataSource"])
@ConfigurationProperties(prefix = "spring.datasource.slave")
fun slaveDataSource(): DataSource = DataSourceBuilder.create()
.type(HikariDataSource::class.java)
.build()
.apply {
isReadOnly = true
}
@Bean
@ConditionalOnBean(name = ["masterDataSource", "slaveDataSource"])
fun routingDataSource(
@Qualifier("masterDataSource") masterDataSource: DataSource,
@Qualifier("slaveDataSource") slaveDataSource: DataSource
): DataSource {
val routingDataSource = RoutingDataSource()
val dataSources: Map<Any, Any> = mapOf("master" to masterDataSource, "slave" to slaveDataSource)
routingDataSource.setTargetDataSources(dataSources)
routingDataSource.setDefaultTargetDataSource(masterDataSource)
return routingDataSource
}
@Primary
@Bean(name = ["currentDataSource"])
@ConditionalOnBean(name = ["routingDataSource"])
fun currentDataSource(routingDataSource: DataSource) = LazyConnectionDataSourceProxy(routingDataSource)
기본적으로 readOnly 는 slave, 그 외에는 master 로 지정한 방식인데요
코드에도 나타나 있듯이 routing 전략과 Datasource 지정하는 것을 완성하는 방식입니다
다만 신기한 것이 있다면, 바로 LazyConnectionDataSourceProxy 인데요
해당 클래스는 요약드리자면,
사용하는 시점에 Connection 을 맺게 해주는 클래스입니다
이에 대한 설명은 추가적으로 제가 작성해드릴게요 ㅎㅎ
+ 아직 테스트 코드가 완성되지 않아서 설명을 추가하여 한번 작성도와드려보겠습니다
참고
Read Write and Read-only transaction
'Developer > Spring' 카테고리의 다른 글
[Test] 효과적인 Integration Test 를 위한 Spring Test Context 를 구성해보자 (2) | 2022.09.24 |
---|---|
[Kotlin/Feign] Apache Http Client5 에 대해 알아보자 (0) | 2021.11.06 |
Spring Batch란? - 기본 요소에 대해 알아보자 (0) | 2021.06.12 |
Spring Boot Context Test - 스프링 컨텍스트 테스트 (aka. IntegrationTest) (0) | 2021.05.05 |
번외편 - Junit LifeCycle과 @ExtendWith 에 대해 알아보자 (0) | 2021.05.02 |