ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [AWS + Spring] EB + Spring Boot + Aurora RDS 연동 및 Read/Write 분산 처리 (3)
    Spring 2023. 1. 14. 17:23
    반응형

    저번 포스팅에선 bastion host ssh turnnel 방식을 사용하여 Intellij DataBase Tool에  Aurora Server를 연동 하고

    Aurora Read Instance(읽기 전용)를 추가했었다.

     

    이번 포스팅에선 Spring Boot에서 ReplicationRouting Configuration을 구성하여 Read-Write 분산 처리를 하고

    Elastic beanstalk에 배포할 것이다.

     

     

    * 참고로 우리는 최종적으로 ElasticBeanstalk에 배포까지 할 예정이기 때문에

    개발 서버(혹은 스테이징, 운영) 환경 기준으로 진행한 것이다. (local이 아님)

    따라서 local(Docker Mysql Container), dev(Aws Aurora Rds) 환경을 나눠줘야 한다.

     

     

    실습환경

    • Intellij Ultimate
    • JAVA 17
    • Spring Boot 2.7.3
    • Maven
    • JPA
    • AWS RDS Aurora(MySQL)
    • 개발 서버 환경 기준 (Not Local)

     

    📌 시작하기

    1. Spring Boot Config 설정

     

    pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>

    dependency 추가

     

     

    docker-compose.yml

    version: '3.1'
    services:
      mysql:
        image: mysql:8.0.22
        platform: linux/amd64
        command:
          - --character-set-server=utf8mb4
          - --collation-server=utf8mb4_general_ci
        restart: always
        ports:
          - 3306:3306
        environment:
          MYSQL_ROOT_PASSWORD: 1234
          MYSQL_USER: test
          MYSQL_PASSWORD: 1234
          MYSQL_DATABASE: test
          TZ: Asia/Seoul

    Mysql Container를 실행하기 위한 docker-compose 파일이다.

    아래 명령어를 실행하여  Mysql Server를 띄우자.

    ~$ docker-compose up -d

     

     

    project-path/src/main/resource/application.yml

    spring:
      profiles:
        active: local

     

    project-path/src/main/resource/application-local.yml

    server:
      port: 8080
    
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test
        username: test
        password: 1234

    url, username, password에 mysql 정보를 입력한다.

     

     

    project-path/src/main/resource/application-develop.yml

    server:
      port: 5000
    
    spring:
      datasource:
        replication:
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: 마스터계정
          password: 마스터계정 패스워드
          write:
            name: write
            url: jdbc:mysql://리전 클러스터(라이터 인스턴스) 엔드포인트:3306/test
          reads:
            - name: read1
              url: jdbc:mysql://리전 클러스터(리더 인스턴스) 엔드포인트:3306/test

     

    리전 클러스터(리더 인스턴스) 엔드포인트를 적어두면 로드밸런싱을 해주지만 AWS RDS Aurora를 사용하지 않고 Read용

       데이터베이스 엔드포인트를 여러개 적어야 할 상황이 있기 때문에 해당 프로퍼티는 List로 구성했다.

     

     

    ReplicationDataSourceProperties

    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.List;
    
    @Getter @Setter
    @Configuration
    @Profile("!local & !test")
    @ConfigurationProperties(prefix = "spring.datasource.replication")
    public class ReplicationDataSourceProperties {
    
        private String username;
        private String password;
        private String driverClassName;
        private Write write;
        private List<Read> reads;
    
        @Getter
        @Setter
        public static class Write {
            private String name;
            private String url;
        }
    
        @Getter
        @Setter
        public static class Read {
            private String name;
            private String url;
        }
    }

    ConfigurationProperties Annotation을 이용해 application.yml에 설정한 내용을 위 클래스와 매핑시킨다.

     

     

     

     

    ReplicationRoutingDataSource

    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    import org.springframework.transaction.support.TransactionSynchronizationManager;
    
    import java.util.List;
    import java.util.Map;
    
    @Profile("!local & !test")
    public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
    
        private static final String READ = "read";
        private static final String WRITE = "write";
        private final ReadOnlyDataSourceCycle<String> readOnlyDataSourceCycle = new ReadOnlyDataSourceCycle<>();
    
    
        @Override
        public void setTargetDataSources(Map<Object, Object> targetDataSources) {
            super.setTargetDataSources(targetDataSources);
            List<String> readOnlyDataSourceLookupKeys = targetDataSources.keySet()
                    .stream()
                    .map(String::valueOf)
                    .filter(lookupKey -> lookupKey.contains(READ)).toList();
            readOnlyDataSourceCycle.setReadOnlyDataSourceLookupKeys(readOnlyDataSourceLookupKeys);
        }
    
        @Override
        protected Object determineCurrentLookupKey() {
            return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
                    ? readOnlyDataSourceCycle.getReadOnlyDataSourceLookupKey()
                    : WRITE;
        }
    }

    💡AbstractRoutingDataSource 

    이 클래스는 DataSource와 각 DataSource 조회할 key로 구성할 수 있게끔 해주는데

    여기서 말하는 key는 Read와 Write 두개를 얘기한다.

     

    그러면 Read용과 Write용 데이터베이스 커넥션을 언제 얻어야 할지 구분을 해야 하는데

    그 구분은 @Transactional 어노테이션으로 구분이 가능하다.

     

    @Transactional에 readOnly 속성이 true로 지정되면 Read 데이터베이스 커넥션을 얻고

    false(기본값)면 Write 데이터베이스 커넥션을 얻는다.

     

     

     

    ReadOnlyDataSourceCycle

    import java.util.List;
    
    public class ReadOnlyDataSourceCycle<T> {
        private List<T> readOnlyDataSourceLookupKeys;
        private int index = 0;
    
        public void setReadOnlyDataSourceLookupKeys(List<T> readOnlyDataSourceLookupKeys) {
            this.readOnlyDataSourceLookupKeys = readOnlyDataSourceLookupKeys;
        }
    
        public T getReadOnlyDataSourceLookupKey() {
            if (index + 1 >= readOnlyDataSourceLookupKeys.size()) {
                index = -1;
            }
            return readOnlyDataSourceLookupKeys.get(++index);
        }
    }

    해당 클래스는 AWS RDS Aurora를 사용하고 있다면 필요는 없지만 간략히 설명하자면 어플리케이션에서 N개의 Read용

    데이터베이스에 로드밸런싱을 해준다고 생각하면 된다.

     

    ReplicationDataSourceConfiguration

    import com.zaxxer.hikari.HikariDataSource;
    import lombok.RequiredArgsConstructor;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.env.Environment;
    import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
    
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    @Configuration
    @RequiredArgsConstructor
    @Profile("!local & !test")
    public class ReplicationDataSourceConfiguration {
    
        final Environment environment;
    
        private final ReplicationDataSourceProperties replicationDataSourceProperties;
    
        @Bean
        public DataSource routingDataSource() {
            ReplicationRoutingDataSource replicationRoutingDataSource = new ReplicationRoutingDataSource();
    
            ReplicationDataSourceProperties.Write write = replicationDataSourceProperties.getWrite();
            DataSource writeDataSource = createDataSource(write.getUrl());
    
            Map<Object, Object> dataSourceMap = new HashMap<>();
            dataSourceMap.put(write.getName(), writeDataSource);
    
            List<ReplicationDataSourceProperties.Read> reads = replicationDataSourceProperties.getReads();
            for (ReplicationDataSourceProperties.Read read : reads) {
                dataSourceMap.put(read.getName(), createDataSource(read.getUrl()));
            }
    
            replicationRoutingDataSource.setDefaultTargetDataSource(writeDataSource);
            replicationRoutingDataSource.setTargetDataSources(dataSourceMap);
            replicationRoutingDataSource.afterPropertiesSet();
    
            return new LazyConnectionDataSourceProxy(replicationRoutingDataSource);
        }
    
        private DataSource createDataSource(String url) {
            HikariDataSource hikariDataSource = new HikariDataSource();
            hikariDataSource.setDriverClassName(replicationDataSourceProperties.getDriverClassName());
            hikariDataSource.setUsername(replicationDataSourceProperties.getUsername());
            hikariDataSource.setPassword(replicationDataSourceProperties.getPassword());
            hikariDataSource.setJdbcUrl(url);
    
            return hikariDataSource;
        }
    
    
    }

    application.yml에 정의된 데이터베이스 접속정보들을 읽어들여서 Write용 DataSource, Read용 DataSource를 생성 후

    Spring에서 라우팅 해줄 수 있게끔 설정한다.

     

    LazyConnectionDataSourceProxy 참고: https://sup2is.github.io/2021/07/08/lazy-connection-datasource-proxy.html

     

     

    💡@Profile("!local & !test")

    본 포스팅에선 local에서는 단순 mysql을 사용하고, 개발(혹은 스테이징, 운영) 환경에서는 Aurora RDS를 사용하는 구조이다.

    그렇기 때문에 application의 설정도 다르다. 즉, local, test 환경에선 ReplicationRouting 설정이 필요 없기 때문에

    @Profile Annotation을 사용하여 local, test 환경에선 Bean이 생성 되지 않게 설정 한 것이다.

     

     

     

    Controller

    import lombok.RequiredArgsConstructor;
    import org.springframework.web.bind.annotation.*;
    
    @RestController
    @RequestMapping("/api/test")
    @RequiredArgsConstructor
    public class TestController {
    
        private final TestService service;
    
        @GetMapping
        public String get() {
            return service.get();
        }
    
        @PostMapping
        public String create() {
            return service.create();
        }
    }

    Test를 위한 간단한 Controller를 생성한다.

     

    Service

    @Slf4j
    @RequiredArgsConstructor
    @Transactional(readOnly = true)
    @Service
    public class TestService {
    
        private final PersonRepository repository;
        private final DataSource lazyDataSource;
    
        public String get() {
            try (Connection connection = lazyDataSource.getConnection() ) {
                log.info("read url : {}", connection.getMetaData().getURL());
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
            
            Person person = repository.findById(1L).orElseThrow(RuntimeException::new);
            return person.getId();
        }
    
        @Transactional
        public void create() {
            try (Connection connection = lazyDataSource.getConnection() ) {
                log.info("write url : {}", connection.getMetaData().getURL());
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
            
            Person person = repository.save(new Person("test name"));
            return person.getId();
        }
    
    }

    Test를 위한 간단한 Service를 생성한다.

    Connection을 얻어와서 연결된 Database url을 log로 출력한다.

    get method에선 Reader Instance Url, create method에선 Write Instance Url이 출력 되어야 한다.

     

    자, 이제 모든 설정이 끝났다. Application을 Elasticbeanstalk에 배포 후, 정상적으로 작동 하는지 테스트 해보자.

     

     

    Maven-Plugin으로 패키징해서 jar 파일을 생성한다.

    Build가 성공적으로 끝나면 target/classes/{패키지명} 안에 projectName-0.0.1.jar 파일이 생성 된다.

    다시 Aws Console로 이동해보자.

     

     

     

    Elastic Beanstalk > 환경 > Test-env 페이지에서 '업로드 및 배포'를 클릭한다.

     

     

     

    '파일 선택'을 클릭해서 생성한 프로젝트의 Jar 파일을 업로드하고 배포한다.

    재기동이 완료 되면 EB의 구성 Tab 으로 이동하여 Active Profile 변수를 설정 해주자.

     

     

    '편집'을 눌러서 환경 속성을 수정하자.

     

     

     

    위 환경 변수를 설정해주면 JVM에서 application-develop.yml의 설정으로 서버를 기동 시켜준다.

    설정을 완료하면 Beanstalk이 재기동 된다. 

    테스트를 진행해보자.

     

     

     

    Test

    Get   : http://Testeb-env.XXXXX.ap-northeast-2.elasticbeanstalk.com/api/test 

    Post : http://Testeb-env.XXXXX.ap-northeast-2.elasticbeanstalk.com/api/test

    write url : jdbc:mysql://auroracluster-XXX:3306/test
    Opened connection [connectionId{localValue:8}] to XXXXX (URL)
    read url : jdbc:mysql://aurora-reader-XXX:3306/test

    @Transactional, @Transactional(readOnly = true) 두개의 메서드를 생성 후 API 요청을 통해 Database Url값을 확인해보면

    정상적으로 라우팅 되어 있는것을 확인할 수 있다. 

     

    또한 Intellij Database Tool에서 연동한 Aurora Instance를 통해서도

    Person Table에 Data가 정상적으로 추가 된 것을 확인할 수 있다.

     

     

    이렇게 해서 Spring Boot에서 Aurora RDS를 연동하여 Replication Routing 처리를 통해 Read/Write

    분산 처리를 한 뒤 Elastic Beanstalk에 배포까지 해봤다.

     

     

     

     

     

    Reference:

    https://kim-jong-hyun.tistory.com/125

    https://velog.io/@yyong3519/Datasource-%EB%B6%84%EB%A6%AC-WriteRead-With-Aurora-MySQL

    반응형

    댓글

Designed by Tistory.