-
[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; } }
이 클래스는 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
반응형'Spring' 카테고리의 다른 글
[AWS + Spring] EB + ElastiCache Redis + Spring Boot 연동 (2) (0) 2023.01.15 [AWS + Spring] EB + ElastiCache Redis + Spring Boot 연동 (1) (0) 2023.01.15 [AWS + Spring] EB + Spring Boot + Aurora RDS 연동 및 Read/Write 분산 처리 (2) (0) 2023.01.04 [AWS + Spring] EB + Spring Boot + Aurora RDS 연동 및 Read/Write 분산 처리 (1) (0) 2023.01.04 [Spring] Spring에서 CORS 처리(설정)하는 방법 (0) 2022.10.27