ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • DB 이중화하기 with Spring AOP
    개발 기록 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
    반응형

    댓글

Designed by Tistory.