개발자의 길

[Springboot] 다중 DB(database) 연결-custom annotation으로 본문

4. JAVA

[Springboot] 다중 DB(database) 연결-custom annotation으로

자르르 2023. 4. 14. 18:31


업무를 하다보니,

 

한개 프로젝트에 다중의 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

 

이라고 찍히면 잘 된것 이다.

 

아...적용할려고, 진짜 오래 걸렸다.



이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공유하기 링크
Comments