[Springboot] 다중 DB(database) 연결-custom annotation으로
업무를 하다보니,
한개 프로젝트에 다중의 db를 연결 할 일이 많다.
대부분의 구글링을 찾다보면,
가장 자주 보이는 예제가 Transactional(readonly=true) 같은 어노테이션으로
master/slave를 나눠서 쓰는 예제가 많이 보인다...
근대 나는 사실 트랜잭션과 상관없이, 다중의 db를 쓰고 싶고..
3개 이상의 다른 connection db 를 사용하고 싶었다..
고민에 고민을 거듭하고, 검색에 검색을 하여서..
custom annotation을 따로 만들어서, 해당 값을 AbstractRoutingDataSource에 determineCurrentLookupKey() 에서 값을 받아 오면 될거 같았다
기본 springboot 셋팅은 했다 치고..
application.yml
spring:
database-names:
list: main, sub1, sub2
datasource:
main:
url: jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=UTC
username: test
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
sub1:
url: jdbc:mysql://sub1.localhost:3306/test?characterEncoding=UTF-8&serverTimezone=UTC
username: test1
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
sub2:
url: jdbc:mysql://sub2.localhost:3306/test?characterEncoding=UTF-8&serverTimezone=UTC
username: test2
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
db는 main,sub1,sub2 3개를 연결한다.
DatabaseConfig.java
package com.example.config.datasource;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Configuration
@RequiredArgsConstructor
public class DatabaseConfig {
@Value("${spring.database-names.list}")
List<String> databaseNames;
private final Environment environment;
public static final String PROPERTY_PREFIX = "spring.datasource.";
/**
*
* 기본적으로 main(dataSource) 접속을 바라본다
* 소스의 Service 단에 > @DbRouting(value = "sub1") 를 넣으면 sub1의 설정을 바라본다.
* 참고로 @DbRouting은 custom annotation으로 만들어서, aspect를 통해 어노테이션 값을 DynamicRoutingDataSource에서 설정되게끔 해놓았다.
* 이거는 다중의 db 셋팅이 필요한 경우에 spring.database-names.list 에 넣고, application.yml에
* spring.datasource.여기 아래로 추가적으로 많이 셋팅하여 사용할수 있다
* by.jangjaeyong
*/
@Bean
public DataSource routingDataSource() {
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = getTargetDataSources();
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(dataSourceMap.get("default"));
return routingDataSource;
}
private Map<Object,Object> getTargetDataSources() {
Map<Object,Object> targetDataSourceMap = new HashMap<>();
for (String dbName : databaseNames) {
DataSource dataSource = DataSourceBuilder
.create()
.driverClassName(environment.getProperty(PROPERTY_PREFIX + dbName + ".driver-class-name"))
.url(environment.getProperty(PROPERTY_PREFIX + dbName + ".url"))
.username(environment.getProperty(PROPERTY_PREFIX + dbName + ".username"))
.password(environment.getProperty(PROPERTY_PREFIX + dbName + ".password"))
.build();;
targetDataSourceMap.put(dbName,dataSource);
}
targetDataSourceMap.put("default",targetDataSourceMap.get(databaseNames.get(0)));
return targetDataSourceMap;
}
@Primary
@Bean(name = "routedDataSource")
public DataSource routedDataSource() {
return new LazyConnectionDataSourceProxy(routingDataSource());
}
@Bean(name = "jdbc")
@Autowired
public NamedParameterJdbcTemplate readOnlyJdbcTemplate(@Qualifier("routedDataSource") DataSource ds) {
return new NamedParameterJdbcTemplate(ds);
}
}
DynamicRoutingDataSource.java
package com.example.config.datasource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.StringUtils;
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> DATABASE_NAME = new ThreadLocal<>();
public static void setDatabaseName(String key) {DATABASE_NAME.set(key);}
public static String getDatabaseName() {
return DATABASE_NAME.get();
}
public static void removeDatabaseName() {
DATABASE_NAME.remove();
}
@Override
protected Object determineCurrentLookupKey() {
String db = DATABASE_NAME.get();
if(!StringUtils.hasLength(db)) db = "main";
log.info("current dataSourceType : {}", db);
return db;
}
}
기본 셋팅은 한 상태고,
이 다음은 custom Annotation을 만들겠다.
aspect를 사용할꺼라, 우선은 dependency 추가
나는 gradle를 사용해서, build.gradle에 내용 추가 (maven을 사용하고 있으면 pom.xml에 추가)
implementation 'org.springframework.boot:spring-boot-starter-aop'
사용할 annotation 파일 생성
DbRouting.java
package com.example.config.datasource;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DbRouting {
String value() default "";
}
aspect 파일 생성
package com.example.config.datasource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
@Order(-10)
public class DataSourceAspect {
private final Logger logger = LoggerFactory.getLogger(getClass());
//defininining where the jointpoint need to be applied
@Pointcut("@annotation(com.nasmedia.ntemplatebatch.common.config.datasource.DbRouting)")
public void annotationPointCut() {
}
// setting the lookup key using the annotation passed value
@Before("annotationPointCut()")
public void before(JoinPoint joinPoint){
MethodSignature sign = (MethodSignature)joinPoint.getSignature();
Method method = sign.getMethod();
DbRouting annotation = method.getAnnotation(DbRouting.class);
if(annotation != null){
DynamicRoutingDataSource.setDatabaseName(annotation.value());
logger.info("Switch DataSource to [{}] in Method [{}]",
annotation.value(), joinPoint.getSignature());
}
}
// restoring to default datasource after the execution of the method
@After("annotationPointCut()")
public void after(JoinPoint point){
if(null != DynamicRoutingDataSource.getDatabaseName()) {
DynamicRoutingDataSource.removeDatabaseName();
}
}
}
만들 파일의 준비는 모두 끝났다.
이제 실제 실행되는 Controller의 Service 안에다가 custom annotation을 만든걸 테스트해보자
@Slf4j
@Service
@RequiredArgsConstructor
public class DemoService {
private final DemoRepository demoRepository;
...
...
...
@DbRouting(value="sub1")
public List<Demo> getList() {
...
...
...
return demoRepository.getList();
}
db를 바꿔 사용할 Service 쪽 메소드에다가 이번에 만든 어노테이션을 넣어주자. 안넣으면 디폴트로 기본 main Db를 연동하고, 다른 db를 연동하고 싶으면 value 에다가 application.yml에 정의한 네이밍을 넣어주면 된다.
실행해보면, log에
DynamicRoutingDataSource.java:26 current dataSourceType : sub1
이라고 찍히면 잘 된것 이다.
아...적용할려고, 진짜 오래 걸렸다.