개발 기록

DB 이중화하기 with Spring AOP

neunggu 2023. 3. 19. 15:12
728x90

목표

데이터 베이스를 이중화하여 슬레이브에서 select를 하려고 한다.

코드는 github에 있다.(https://github.com/neunggu/score.git)

 

GitHub - neunggu/score

Contribute to neunggu/score development by creating an account on GitHub.

github.com

 

환경

java 17

spring boot 2.7.6

spring-boot-starter-aop 사용

 

방법

1. 마스터, 슬레이브 2개의 DataSource를 생성해 AbstractRoutingDataSource에 담는다.

(세션에 접속할 db를 선정해 두고 사용할 예정)

(determineCurrentLookupKey 구현 필요)

@Configuration
public class DatabaseConfig {

    @Value("${db.driver-class-name}")
    private String driver;
    @Value("${db.username}")
    private String user;
    @Value("${db.password}")
    private String pw;
    @Value("${db.master.url}")
    private String masterUrl;
    @Value("${db.slave.url}")
    private String slaveUrl;

    @Bean
    public DataSource createRouterDatasource() {
        AbstractRoutingDataSource routingDataSource = new RoutingDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("current:master", createDataSource(driver, masterUrl, user, pw));
        targetDataSources.put("current:slave", createDataSource(driver, slaveUrl, user, pw));
        routingDataSource.setTargetDataSources(targetDataSources);
        return routingDataSource;
    }

    private DataSource createDataSource(String driver, String url, String user, String password) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setConnectionInitSql("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;");
        dataSource.setUsername(user);
        dataSource.setPassword(password);
        dataSource.setJdbcUrl(url);
        dataSource.setMaxLifetime(30000);
        return dataSource;
    }
}
public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        Object dbKey;
        if(RequestContextHolder .getRequestAttributes() == null
            || RequestContextHolder.getRequestAttributes()
                .getAttribute("db_key", RequestAttributes.SCOPE_SESSION) == null) {
            dbKey = "master";
        } else {
            dbKey = RequestContextHolder
                    .getRequestAttributes()
                    .getAttribute("db_key", RequestAttributes.SCOPE_SESSION);
        }
        return "current:" + dbKey;
    }
}

 

2. 세션에 선택된 db를 셋팅하는 유틸 생성.

  (3번에서 만들 aspect에서만 사용할 것이어서 굳이 클래스로 뺄 필요는 없다.)

@Component
public class DBSessionConfigUtil {

    public void setDbKey(HttpSession httpSession, String dbKey) {
        if(httpSession != null && httpSession.getAttribute("db_key") != null ) {
            String dbSessionkey = (String) httpSession.getAttribute("db_key");
            if(!dbSessionkey.equals(dbKey)) {
                httpSession.setAttribute("db_key", dbKey);
            }
        } else {
            httpSession.setAttribute("db_key", dbKey);
        }
    }
}

 

3. Aspect 생성

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SlaveDB {
}
@Aspect
@Component
@RequiredArgsConstructor
public class DB{

    private final HttpSession httpSession;
    private final DBSessionConfigUtil dbSessionUtil;

    @Around("@annotation(SlaveDB)")
    public Object setSlave(ProceedingJoinPoint joinPoint) {
        Object result = null;
        try {
            dbSessionUtil.setDbKey(httpSession, "slave");
            result = joinPoint.proceed();
        } catch (Throwable e) {
        } finally {
            dbSessionUtil.setDbKey(httpSession, "master");
        }
        return result;
    }
}

4. 필요한 곳에 AOP 사용

@Mapper
public interface ScoreMapper {

    @SlaveDB
    @Select("SELECT * FROM score WHERE user_id = #{userId}")
    ScoreEntity findById(String userId);
    
    ...
}

 

결론: AOP를 이용하면 편한 점들이 꽤 있다.

728x90
반응형