[토이 프로젝트] 데이터소스 라우팅 이슈 : AOP 기반 데이터소스 라우팅 어노테이션
카테고리: Ayu-coupon
태그: Database
들어가며
Spring Boot는 기본적으로 데이터소스 하나를 기준으로 되어 있습니다.
그런데 개발을 하다보면 데이터소스는 최소 두 개 이상이 필요한 경우가 많이 생깁니다.
보통 예를 들면 Mysql을 Primary/Secondary 구조의 형태로 따로 접근 해야하는 경우, AbstractRoutingDataSource를 확장하여 트랜잭션의 읽기/쓰기 모드에 따라 데이터소스를 라우팅 하는 방식으로 많이 개발합니다.
하지만 DB의 Repication을 사용하여 Primary/secondary 구조를 만들었다고 해도 복제 지연
과 같은 이슈가 존재하기 때문에, 데이터의 실시간성이 중요한 데이터일 경우 트랜잭션 모드가 읽기 모드라도 Primary 데이터소스에 접근해야할 경우도 있습니다.
이번 포스팅은 트랜잭션의 모드가 아닌, 어노테이션 기반으로 데이터소스를 라우팅하는 방법에 대해 작성해보려 합니다.
전체 코드는 깃허브을 참고해주세요.
어노테이션을 활용한 데이터 소스 라우팅
설정 파일로 데이터소스를 라우팅하는 것은 복잡하고 번거롭습니다.
그렇다면 아래와 같이 어노테이션만 선언함으로써, 데이터소스를 라우팅할 수 있도록 하는 것은 어떨까요?
@Service
@RequiredArgsConstructor
public class IssueUserCouponService {
private final IssueUserCouponModule issueUserCouponModule;
private final IssueValidator issueValidator;
// 어노테이션 선언만으로 라우팅 가능
@DataSource("primary")
@Transactional
public Long issue(IssueUserCouponCommand command) {
issueValidator.validate(command);
return issueUserCouponModule.issue(command);
}
}
위 그림처럼 어노테이션만으로 데이터소스를 라우팅하기 위해선, 라우팅을 위한 어노테이션과 데이터소스의 이름을 저장하기 위한 클래스를 작성해야 합니다.
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
// 데이터소스 이름
String value();
}
===
public class DataSourceNameContextHolder {
// ThreadLocal를 사용하여, 데이터소스 이름 저장
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceName(String dataSourceName) {
Assert.hasText(dataSourceName, "DataSource name must has text");
contextHolder.set(dataSourceName);
}
public static String getDataSourceName() {
return contextHolder.get();
}
public static void clear() {
contextHolder.remove();
}
}
Spring MVC에서는 클라이언트의 요청을 Tomcat 스레드가 처리하고, 스레드마다 트랜잭션을 통해 가져오는 데이터소스가 다를 수 있기 때문에 ThreadLocal
을 사용하여 데이터소스의 이름을 저장하도록 하였습니다.
트랜잭션이 데이터소스를 가져올 때, 아래의 코드처럼 DataSourceNameContextHolder
클래스를 활용하여 함으로써 어노테이션을 통해 저장된 데이터소스 이름을 가져와서, 데이터소스 라우팅을 진행하도록 코드를 작성했습니다.
public class MyRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 저장된 데이터소스 이름 반환
return DataSourceNameContextHolder.getDataSourceName();
}
}
AOP 기반의 데이터소스 라이팅
실제로 아래의 코드처럼 AOP 기반으로, 클래스 또는 메서드의 어노테이션 값을 확인하여, 하나의 API 요청에서 다양한 데이터소스를 사용할 수 있도록 하였습니다.
@Aspect
@Order(value = 0)
@Slf4j
public class DataSourceAspect {
@Around("@annotation(com.ayucoupon.common.aop.multidatasource.DataSource)")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
// 클래스 or 메소드 어노테이션 가져오기
DataSource dataSource = findDataSourceAnnotation(joinPoint);
// 어노테이션 값(데이터 소스 이름) 가져오기
String currentSettingDataSourceName = dataSource.value();
String currentDataSourceName = DataSourceNameContextHolder.getDataSourceName();
// 중간에 데이터 소스 변경시, 변경된 데이터 소스 이름 저장
if (currentSettingDataSourceName != null &&
!currentSettingDataSourceName.equals(currentDataSourceName)) {
DataSourceNameContextHolder.setDataSourceName(currentSettingDataSourceName);
}
try {
// 메서드 호출
return joinPoint.proceed();
} finally {
DataSourceNameContextHolder.clear();
}
}
... 코드 생략
정리
API의 특성에 따라 사용되는 데이터소스가 다를 수 있고, 하나의 API에서도 다양한 데이터소스가 필요할 수 있습니다.
위처럼 단순히 어노테이션만 선언한다면 지정된 데이터소스를 라우팅할 수 있도록 만듦으로써, 간단하고 편하게 데이터소스를 라우팅하도록 개발하였습니다.
끝까지 봐주셔서 감사합니다!
댓글 남기기