
V2 개발 완료 후, 1000명이 동시에 입찰을 시도하는 부하 테스트를 진행했습니다. 테스트 결과, 입찰 API의 평균 응답 시간은 8.5초로 나타났습니다.
이는 팀 내 다른 로직들의 평균 응답 시간(1초 내외)에 비해 현저히 느린 수치였습니다.
문제를 분석한 결과, 입찰 로직에서 3번의 DB 조회와 2번의 DB 업데이트로 인해 DB 서버와 너무 빈번하게 통신을 하고 있다는 결론에 도달했습니다. 이로 인해 시스템의 성능과 안정성을 개선할 방법이 필요했고, 팀 내에서 CQRS(Command Query Responsibility Segregation) 도입을 검토하게 되었습니다. 이후 CQRS를 적용해 문제를 해결할 수 있었습니다.
CQRS
Command(쓰기)와 Query(읽기)를 분리해 성능과 확장성을 높이는 아키텍처 패턴입니다.
실제로는 AWS에 DB 서버를 두 개 띄우고 있지만 서버 비용적인 문제로... 개인적으로는 도커를 이용해 Master DB, Slave DB를 올리려고 합니다.
Master, Slave DB를 올리고 나서는 /etc/my.cnf 파일에 아래 설정을 추가해줍니다.
[mysqld]
server-id = 1
log-bin = mysql-bin
[mysqld]
server-id = 2
log-bin = mysql-bin
relay_log = /var/lib/mysql/mysql-relay-bin
log_replica_updates = ON
read_only
이후, Master DB에 접속해 터미널에 아래의 명령어를 입력합니다.
명령어 입력 후, 나오는 File과 Position을 기억해야 합니다.
mysql> SHOW MASTER STATUS;
Slave DB에 접속해 터미널에 아래의 명령어를 입력합니다.
mysql> CHANGE MASTER TO MASTER_HOST='{MASTER의 네트워크 IP}',
MASTER_USER='{root or 설정한 유저 아이디}',
MASTER_PASSWORD='{유저 계정의 비밀번호}',
MASTER_LOG_FILE='{MASTER의 File}',
MASTER_LOG_POS={MASTER의 Position};
mysql> START SLAVE;
mysql> SHOW SLAVE STATUS\G;
마지막 명령어를 실행했을 때 Slave_IO_Running과 Slave_SQL_Running이 모두 Yes라면 제대로 설정이 된 것입니다.
만약에 둘 중에 하나라도 No로 표시된다면 아래에 오류에 대한 내용이 적혀있으므로 보고 해결해주면 됩니다.
이렇게 Master DB에서 Slave DB로의 동기화를 완료했습니다.
spring:
datasource:
master:
jdbc-url: ${MASTER_DB_URL}
username: root
password: ${MASTER_DB_PW}
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
jdbc-url: ${SLAVE_DB_URL}
username: root
password: ${SLAVE_DB_PW}
driver-class-name: com.mysql.cj.jdbc.Driver
조회의 경우에는 Slave DB를, 그 외의 경우에는 Master DB를 사용할 것이므로 동적으로 DataSource를 결정할 수 있어야 합니다.
AbstractRoutingDataSource를 사용해 동적으로 DataSource를 결정했습니다.
기본값은 Master DB로 설정하고, AOP를 활용해 @Transaction 어노테이션이 붙은 메서드가 호출되기 전에 DataSource를 결정하도록 구현했습니다. readOnly = true인 경우 Slave DB를, 그렇지 않은 경우 Master DB를 사용하도록 설정했습니다.
@Before("@annotation(transactional)")
public void setDataSourceType(Transactional transactional) {
if (transactional.readOnly()) {
DataSourceContextHolder.setDataSourceType("SLAVE");
} else {
DataSourceContextHolder.setDataSourceType("MASTER");
}
}
Master DB에서 Slave DB로의 데이터 복제는 비동기적으로 이루어지기 때문에, 복제 지연이 발생할 수 있습니다. 이를 보완하기 위해 Redis를 캐시 계층으로 활용했습니다. 조회 요청 시 Redis에 데이터가 있는지 먼저 확인하고, 없을 경우에만 Slave DB를 조회하도록 구현했습니다.
Redis에 저장된 데이터는 30분 후 만료되도록 TTL을 설정했습니다. 또한, Master DB에 데이터가 업데이트될 때마다 Redis의 해당 데이터를 덮어쓰도록 구현해 데이터 일관성을 유지했습니다.
CQRS 적용 후, 동일한 조건에서 테스트를 진행한 결과 입찰 API의 평균 응답 시간은 4.9초로 나타났습니다.
이는 CQRS 적용 전 평균 8.5초에서 4.9초로 약 42.35% 단축된 수치이며,
처리량은 61.4/sec에서 85.4/sec로 약 39.09% 향상되었습니다.
이러한 결과를 통해 CQRS 도입이 해당 API의 성능을 크게 개선했음을 확인할 수 있었습니다.