gi_dor

[MySQL] RDS DB 이중화 읽기전용 + CQRS 본문

DataBase

[MySQL] RDS DB 이중화 읽기전용 + CQRS

기돌 2024. 8. 19. 17:20

CQRS 패턴에 대해 알게되었다
CQRS는 데이터 저장소에 대한 읽기와 업데이트(쓰기) 작업을 구분하는 패턴이라고 한다

CQRS는 명령모델과 쿼리모델로 나뉜다

1. 명령모델 Command Model

  • 데이터를 변경하는 작업  (UPDATE , INSERT, DELETE) 를 처리한다 
  • 특정 명령을 받아서 비즈니스 로직과 유효성 검사를 수행하고 데이터를 변경한다

2. 쿼리모델 Query Model

  • 데이터를 조회하는 작업 ( SELECT )을 처리한다
  • 최적화된 일기작업을 위해 데이터를 별도의 형태로 저장하기도 한다

명령모델은 주로 서비스로직과 관련이있고 데이터 생성 , 수정 , 삭제 같은 작업을 담당한다
쿼리모델은 데이터베이스나 다른 저장소에서 데이터를 읽고 결과를 반환하는 역할을 담당한다

 

 

 

DB 이중화 ( 정보처리기사 )

  • 데이터베이스 이중화(Database Replication) 
  • 데이터베이스에 문제 발생 시 백업으로서의 역할을 할수도 있다
  • 데이터베이스로 분산시켜 처리하므로 쿼리 요청을 분산해 데이터베이스의 부하를 줄일 수 있다ㅏ.

 


앞에 RDS 만드는 글을 보셨으면 복제본 생성은 크게 어려울것 없습니다


SpringBoot 프로젝트에 DataSourceConfig, RoutingDataSource 클래스 작성

package com.example.bookhub.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;

import javax.sql.DataSource;
import java.util.HashMap;

@Slf4j
@Configuration
public class DataSourceConfiguration {

    private static final String MASTER_SERVER = "MASTER"; // 마스터 서버를 나타내는 상수 문자열입니다.
    private static final String REPLICA_SERVER = "REPLICA"; // 복제 서버를 나타내는 상수 문자열입니다.

    @Bean
    @Qualifier(MASTER_SERVER)
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create() // 'spring.datasource.master'로 시작하는 구성 속성을 사용하여 DataSource를 작성합니다.
                .build();
    }

    @Bean
    @Qualifier(REPLICA_SERVER)
    @ConfigurationProperties(prefix = "spring.datasource.replica")
    public DataSource replicaDataSource() {
        return DataSourceBuilder.create() // 'spring.datasource.replica'로 시작하는 구성 속성을 사용하여 DataSource를 작성합니다.
                .build();
    }

    @Bean
    public DataSource routingDataSource(
            @Qualifier(MASTER_SERVER) DataSource masterDataSource,
            @Qualifier(REPLICA_SERVER) DataSource replicaDataSource
    ) {
        RoutingDataSource routingDataSource = new RoutingDataSource(); // 사용자 정의 RoutingDataSource 인스턴스를 생성합니다.

        HashMap<Object, Object> dataSourceMap = new HashMap<>(); // 데이터 소스를 보관할 맵을 생성합니다.
        dataSourceMap.put("master", masterDataSource); // "master" 키로 마스터 DataSource를 맵에 넣습니다.
        dataSourceMap.put("replica", replicaDataSource); // "replica" 키로 복제 DataSource를 맵에 넣습니다.

        routingDataSource.setTargetDataSources(dataSourceMap); // 라우팅을 위한 대상 데이터 소스를 설정합니다.
        routingDataSource.setDefaultTargetDataSource(masterDataSource); // 기본 대상 데이터 소스를 마스터 DataSource로 설정합니다.

        return routingDataSource; // routingDataSource를 반환합니다.
    }

    @Bean
    @Primary
    public DataSource dataSource() {
        DataSource determinedDataSource = routingDataSource(masterDataSource(), replicaDataSource()); // 라우팅 로직을 기반으로 적절한 DataSource를 결정합니다.
        return new LazyConnectionDataSourceProxy(determinedDataSource); // 지연 연결 DataSource 프록시를 반환합니다.
    }

}
// Properties DB 설정 읽어오기
@ConfigurationProperties(prefix = "spring.datasource.master")
@ConfigurationProperties(prefix = "spring.datasource.replica")

 

package com.example.bookhub.config;

import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@Slf4j
public class RoutingDataSource extends AbstractRoutingDataSource {
    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        String lookupKey = TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "replica" : "master";
        log.info("Current DataSource is {}", lookupKey);

        return lookupKey;
    }
}

 

isCurrentTransactionReadOnly 이 메서드로 트랜젝션이 readOnly=true일 때는 replica를 반환

실제 replica를 사용 , master를 사용 하는 로직은 AbstractRoutingDataSource에 구현

 

# master db
spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.master.jdbc-url=jdbc:mysql://bookhub.cb044koayrwz.ap-northeast-2.rds.amazonaws.com:3306/bookhub
spring.datasource.master.username=admin
spring.datasource.master.password=ENC(frdBmVOFlCns7XbOH3cDVuObdEx1UE/m)

## read replica
spring.datasource.replica.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.replica.jdbc-url=jdbc:mysql://bookhub-replica.cb044koayrwz.ap-northeast-2.rds.amazonaws.com/bookhub
spring.datasource.replica.username=admin
spring.datasource.replica.password=ENC(HZz6V+9Ztm04oY0SfI53pPzvCjuIwd8B)

 

Spring Security 를 사용한 로그인 메서드

/**
 * 주어진 사용자 아이디를 기준으로 사용자의 데이터를 가져와 UserDetails 객체로 반환합니다.
 * @param id 사용자 아이디
 * @return UserDetails 객체
 * @throws UsernameNotFoundException 주어진 아이디에 해당하는 사용자를 찾을 수 없는 경우 발생합니다.
 */
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {

    // 사용자 아이디를 기준으로 데이터베이스에서 사용자 정보를 가져옵니다. 이 정보는 user 객체에 저장
    User user = userMapper.selectUserById(id);

    // 데이터베이스에서 가져온 사용자 정보가 없다면(null이면) 예외를 발생시킵니다.
    if(user == null) {
        throw new UsernameNotFoundException("등록되지 않은 사용자이거나 탈퇴처리된 사용자입니다. : " +id);
    }

    if ("Y".equals(user.getDelYn())) {
        throw new UsernameNotFoundException("회원탈퇴 처리된 계정입니다 : " +id);
    }


    // UserDetailsImpl 클래스의 객체를 생성합니다. 이 객체는 사용자의 인증 및 권한 정보를 제공하기위해 사용한다
    UserDetailsImpl userDetails = new UserDetailsImpl();

    // 객체에서 가져온 사용자 아이디와 비밀번호를 userDetails 객체에 설정
    userDetails.setId(user.getId());
    userDetails.setPassword(user.getPassword());

    userDetails.setAuthorities(List.of(new SimpleGrantedAuthority("ROLE_USER")));

    System.out.println("서비스 - 로그인한 아이디 : " +user.getId());
    return userDetails;
}

읽기 전용 replica 적용

 

읽기 적용 하지 않은 메인 페이지 책 목록 가져오는 메서드


EC2 연결 후 배포 에서 확인하기

728x90