데이터베이스 다중화1.1 목적1.2 개념1.3 스프링에서는 어떻게 할까 ??1.4 Replica 관련 개념 잡기 좋은 영상다중 datasource Transaction 관리2) 캐시2.1 캐시란 ?2.2 로컬 캐시장점단점2.3 글로벌 캐시장점단점3) CDN (Contents Delivery Network)3.1 CDN 이란?3.2 사용 사례5) DB Lock낙관적 락 (Optimistic Lock)비관적 락 (Pessimistic Lock)s Lock(수정만 락 적용)x Lock분산락 CQRS
데이터베이스 다중화
1.1 목적
대부분의 애플리케이션은 읽기 연산의 비중이 쓰기 연산보다 훨씬 높다.
- 컬리같은 경우를 봐도 하나 상품 조회하면 후기, 상품정보, 유저 등급 등등 가져와야할 도메인이 다른 정보가 꽤나 많음. RDB로 하면 다 조인해서 가져와야 하는데 NoSQL로 하면 하나의 row에 데이터를 다 넣을 수 있음
따라서 더 나은 성능을 위하여 데이터 변경은 주 데이터 베이스로 읽기 연산은 부 데이터베이스 서버들로 분산한다.
1.2 개념

주 데이터베이스 (Master)
- 쓰기 연산 (INSERT, UPDATE, DELETE) 지원
부 데이터베이스 (Slave)
- 주 데이터베이스로 부터 사본을 전달 받는다.
- 읽기 연산 (SELECT) 지원
1.3 스프링에서는 어떻게 할까 ??
하나의 데이터소스를 사용할 경우 아래와 같이 설정파일을 작성하면 스프링에서 자동으로 데이터소스를 생성한다.
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/kotlin_spring?serverTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 1234
하지만 두개 이상의 데이터소스를 사용하는 경우 스프링에서 자동으로 데이터소스를 생성하지 않기 때문에 아래와 같이 작성을 하면 추가적인 코드가 필요하다.
spring: datasource: master: hikari: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/multiple-datesource?serverTimezone=UTC read-only: false username: root password: 1234 slave: hikari: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/multiple-datesource?serverTimezone=UTC read-only: true username: root password: 1234
간단하게 어떤 식으로 작성하는지 살펴보자.
우선 등록한 데이터소스에 대한 Bean 을 수동으로 등록을 해줘야한다.
@Configuration public class MasterDataSourceConfig { @Primary @Bean(name = "masterDataSource") @ConfigurationProperties(prefix="spring.datasource.master.hikari") public DataSource masterDataSource() { return DataSourceBuilder.create() .type(HikariDataSource.class) .build(); } } @Configuration public class SlaveDataSourceConfig { @Bean(name = "slaveDataSource") @ConfigurationProperties(prefix="spring.datasource.slave.hikari") public DataSource slaveDataSource() { return DataSourceBuilder.create() .type(HikariDataSource.class) .build(); } }
그 다음 스프링의 트랜잭션 readOnly 옵션에 따라 어떤 데이터 소스를 사용할지에 대한 분기 처리가 필요하다.
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { DataSourceType dataSourceType = TransactionSynchronizationManager .isCurrentTransactionReadOnly() ? DataSourceType.Slave : DataSourceType.Master; return dataSourceType; } } public enum DataSourceType { Master, Slave }
그 외 추가적인 설정이 필요하지만 어떤 식으로 코드로 작성하는지는 이 정도로 마무리 지으려고 한다.
좀 더 자세한 코드를 보고 싶다면 아래 문서를 참고하길 바란다.
[ Cheese 10Yun ] Spring 레플리케이션 트랜잭션 처리 방식
위 예제를 살펴보면, 설정할 코드들이 상당히 많은 것을 알 수 있다.
AWS Aurora MySQL 을 사용하면서 MariaDB Connector / J 를 사용한다면 위 예제와 같이 복잡한 코드를 작성할 필요가 없다.
아래와 같이 데이터소스를 마스터 하나만 등록을 하고 읽기 트랜잭션만 명시하면 자동으로 요청 분기가 처리된다.
spring: datasource: url: jdbc:aurora:mysql://127.0.0.1:3306/kotlin_spring?serverTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 1234
단 최신 버전의 MariaDB Connector / J 에서는 지원하고 있지 않기 때문에 추후 다른 드라이버로 교체할 필요가 있다.
하지만 현재는 MariaDB Connector / J 말고는 대안이 없기 때문에 현업에서도 해당 드라이버로 사용하고 있다.
AWS MySQL JDBC 가 있지만, 아직 개발은 되지 않았다.
PR 목록을 보면 Read-write splitting 개발은 진행이 되고 있는 것 같다.

아래 PR 에서는 1.2.0 version 에 릴리즈가 될 것 같다. (2023-07-08 기준으로 1.1.8 version 까지 릴리즈 됨)
1.4 Replica 관련 개념 잡기 좋은 영상
우형 - 테코톡
다중 datasource Transaction 관리
- JtaTransactionManager (2Phase Commit) : 이 방법은 네트워크 지연으로 인해서 문제가 생길수도 있고,
- 2 phase이기 때문에 그만큼 DB connection 및 lock이 걸리는 시간이 길어지기 때문에 성능 저하가 발생할 수 있습니다.
- 특정 DB는 2 phase commit을 지원하지 않을 수도 있습니다.
2phase Commit
일부 트랜잭션만 성공하는 경우를 막아주는 원자적(atomic)인 트랜잭션
write data
: Database node에 데이터 쓰거나 읽을 준비
prepare(phase 1)
: commit이 가능한 지 확인
commit(phase 2)
: commit 수행
SAGA 패턴(보상 트랜잭션)

2) 캐시
2.1 캐시란 ?

값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고 사용하도록 하는 저장소이다.
캐시의 장점은 데이터베이스에 직접 조회하는 것 보다 성능이 좋을 뿐만 아니라 데이터베이스 부하를 부하를 줄일 수 있다.
만약 아래 그림 처럼 공지사항과 같은 동일한 결과를 반본적으로 돌려주는 API 가 있다고 생각하자.
이 API는 요청을 받으면 매번 Controller --> Service --> Repository 를 거친다음 DB 조회 및 로직을 처리하는 과정을 반복적으로 진행한다.
즉, 동일한 결과를 보여주는 작업을 반복적으로 진행하기 때문에 비효율적이다.

위와 같은 상황에서 캐시를 사용한다면 첫 번째 요청 이후 부터는 캐시에 저장되어 있는 데이터를 바로 읽어서 전달하면 되기 때문에 시스템 부하를 줄일 수 있다.
2.2 로컬 캐시

장점
- 네트워크 호출 x, 서버의 물리 메모리에 직접 접근하기 때문에 빠르다.
단점
- 서버가 여러대인 경우 동기화 문제가 있다.
- 인스턴스 물리 메모리 사이즈 제약이 있다.
2.3 글로벌 캐시

장점
- 서버 동기화를 걱정할 필요가 없다
단점
- 네트워크 호출이 필요하다
- 상대적으로 로컬 캐시 보다 느리다.
- 캐시 서버 장애 대비가 필요하다.
+ 읽어 볼 만한 글
3) CDN (Contents Delivery Network)
3.1 CDN 이란?
정적 콘텐츠를 전송하는 데 쓰이는 분산된 서버
대표적인 CDN 벤더인 Akamai 문서에는 아래와 같이 소개하고 있다. [문서 링크]

3.2 사용 사례
특정 사이트에서 개발자 도구로 이미지 URL 를 확인해보자.
5) DB Lock
낙관적 락 (Optimistic Lock)
트랜잭션이 커밋될 때, 데이터베이스는 격리가 위반되었는지 체크한다.
만약, 위반하였다면 해당 트랜잭션은 Rollback 한다.
경쟁이 심하지 않은 상황이라면 낙관적락이 비관적락보다 비교적 성능이 좋다.
하지만 경쟁이 심하다면 Rollback 비율이 높아지기 때문에 성능이 떨어진다.
@Entity @OptimisticLocking(type = OptimisticLockType.VERSION) public class Product { @Id private Long id; private String name; @Version private Long version; }
비관적 락 (Pessimistic Lock)
각 트랜잭션이 실행이 되는 동안 전체 데이터베이스에 독점 잠금을 획득한다.
즉, 락이 걸린 상태에서 다른 트랜잭션은 락이 끝날때까지 대기하는 상태가 된다.
개별 트랜잭션의 성능을 향상 시키는 방법 말고는 락 시간을 줄이는 방법을 찾기 어렵다.
s Lock(수정만 락 적용)
- 다른 사용자가 동시에 읽을 수는 있지만, Update Delete 를 방지함
- JPA: PESSIMISTIC.READ
x Lock
- 다른 사용자가 읽기, 수정, 삭제 모두를 불가능하게 함
- JPA: PESSIMISTIC.WRITE
분산락
- 분산락이란 여러서버에서 공유된 데이터를 제어하기 위해 사용
- 분산락 저장소로 Redis 를 많이 사용하고 ZooKeeper(많이는 안씀) 를 사용하여 구현할 수 있다.
- JAVA 와 Redis 를 사용한다면 Redisson 을 사용하여 쉽게 분산락을 사용할 수 있다.
- 키워드: 스핀락, Pub/Sub
- 스핀락은 부하가 많아서 Redisson 에서는 Pub/Sub 방식으로
- redis도 Pub/Sub을 지원해줌
+ 분산락 읽어 볼 만한 글
CQRS
Command Query Responsibility Segregation
[우아콘 2021 유튜브 ] B 마트 전시 도메인 CQRS 적용하기