진행중인 프로젝트
https://github.com/Hwangwonuk/cucumber-market
GitHub - Hwangwonuk/cucumber-market: 중고물품 거래사이트 오이마켓 토이프로젝트
중고물품 거래사이트 오이마켓 토이프로젝트. Contribute to Hwangwonuk/cucumber-market development by creating an account on GitHub.
github.com
개요
- 원인 : cucumber-market의 Service Layer에 속해있는 거의 모든 메소드가 데이터베이스에 쿼리 요청을 보냅니다.
- 문제 : 많은 사람들이 서비스를 사용하고 있다면 현재 사용하고 있는 MySQL 서버가 Scale UP되어 있더라도
서버 한대가 모든 트래픽을 견디기에는 한계가 있습니다. - 결과 : 그로 인해 데이터베이스에 장애가 발생한다면 운영 중인 서비스가 바로 타격 을 입게됩니다.
- 따라서, Replication을 사용 하여 MySQL의 Master - Slave 환경을 구축 했습니다.
MySQL Replication
아래 그림은 가장 자주 사용되는 Replication 형태인 1:M 복제입니다.
1:M 복제는 하나의 마스터 MySQL 서버에 2개 이상의 슬레이브 MySQL 서버를 연결시키는 형태입니다.
일반적으로 마스터 서버는 INSERT , UPDATE , DELETE 의 생성, 변경 작업을 담당하며
슬레이브 서버는 SELECT 의 읽기 작업을 담당합니다.
MySQL을 Replication을 통해 서버 환경을 구축하면 크게 두 가지 장점을 얻을 수 있습니다.
- 부하를 분산시킬 수 있다.
- 가용성이 높아진다.
보통 MySQL에서 키를 추가하는 작업은
디스크로부터 인덱스 페이지를 읽고 쓰기를 해야 하기 때문에
SELECT문에 비해 INSERT 나 UPDATE 문장을 처리하는데 상대적으로 시간이 더 오래 걸립니다.
웹 서비스에서는 쓰기 작업보다 읽기 작업의 비중이 훨씬 높습니다.
그렇기 때문에 모든 작업을 하나의 서버에서 모두 처리하고자 한다면
병목현상이 발생하여 다른 작업들의 처리까지 늦어지게 될 수 있습니다.
따라서, 서버를 확장하고 최대한 읽기 작업을 슬레이브 서버에서 처리한다면
높은 부하를 견딜 수 있는 구조가 됩니다.
만약 MySQL 서버가 한 대인 상황에서 장애가 발생한다면
관련된 모든 서비스들이 멈춰버리는 심각한 상황이 발생할 것입니다.
하지만 Replication을 구성한다면 이런 장애 상황에서 슬레이브 서버를 마스터로 승격시켜서
간단히 서비스를 복구하는 것이 가능합니다.
슬레이브 서버를 마스터로 승격시키기 위해 몇 가지 작업이 필요하지만
비교적 짧은 시간안에 가능하기 때문에 서비스를 지속적으로 이어나갈 수 있게됩니다.
트랜잭션 단위로 쿼리 요청을 Master - Slave로 분기하기
Spring에서는 여러 개의 DataSource 를 하나로 묶고 자동으로 분기해주는
AbstractRoutingDataSource 클래스를 제공합니다.
만약 트랜잭션 단위가 아닌 쿼리 단위로 분기한다면 하나의 메소드 안에 쓰기 작업과 읽기 작업이 동시에 있는 경우,
마스터 서버에서 커밋된 데이터라 하더라도 커밋된 시점에
슬레이브에는 Replication이 반영되지 않았을 수도 있습니다. -> 트랜잭션이 적용되어 있어서
이렇게 되면 '없는 데이터'라는 응답을 받게 되므로 트랜잭션 단위로 분기하게 되었습니다.
또한, 현재 트랜잭션의 속성에 따라 사용할 DataSource 를 결정할 것이기 때문에
TransactionSynchronizationManager 와 함께 사용할 것입니다.
AbstractRoutingDataSource 로직
determineCurrentLookupKey() 로직
트랜잭션의 속성에 따라 targetDataSources 맵의 조회 키를 결정하기 위해
AbstractRoutingDataSource 클래스를 상속받아 determinCurrentLookupKey() 로직을 구현합니다.
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
return isReadOnly ? "slave" : "master";
}
}
프록시 객체와 지연 로딩으로 DataSource 분기 처리
우선 시작에 앞서 스프링의 트랜잭션 동기화에 대해서 알아야 합니다.
트랜잭션 동기화란?
트랜잭션을 시작하기 위해 만든 Connection 객체를 특별한 저장소에 보관해두고,
이후에 호출되는 DAO 메소드에서 저장된 Connection 객체를 가져다가 사용하는 것입니다.
즉, 몇 번의 쿼리가 반복되더라도 Connection 을 종료시키지 않고, 보관해둔 뒤
다음 과정에서도 계속 이용하면서 하나의 트랜잭션으로 묶는 것입니다.
트랜잭션 동기화는 위의 그림에 나와있는 순서대로 진행됩니다.
여기서 문제가 되는 것은 Connection 객체를 획득하는 시기입니다.
AbstractRoutingDataSource 에서 트랜잭션 속성에 따라 DataSource 를 분기하였고,
이에 따라 Connection 객체를 리턴할 수 있도록 코드를 작성했습니다.
이 때, 트랜잭션 속성은 TransactionSynchronizationManager 가 관리합니다.
그러나 TransactionSynchronizationManager 는 동기화가 시작되기 전에는 해당 트랜잭션 속성을 알 수 없습니다.
그렇기 때문에 분기 처리가 실패하게 됩니다.
해결방법
스프링이 트랜잭션이 실행되기만 하면 Connection 객체를 얻으려고 하기 때문에 문제가 발생합니다.
아래 그림과 같이 TransactionManager 에게 AbstractRoutingDataSource 에 대한 프록시 객체를
리턴하게 만들어 주면 됩니다.
LazyConnectionDataSourceProxy 를 사용하면 실제 쿼리를 호출하기 전까지
JDBC Connection 을 가져오지 않습니다.
따라서, 트랜잭션 동기화가 시작되고 대상 메소드 안에서 쿼리가 발생한 후에
우리가 AbstractRoutingDataSource 에 작성한 로직대로
올바른 DataSource 에 대한 Connection 을 얻어 쿼리를 실행할 수 있습니다.
프록시 객체와 지연 로딩을 통해 쿼리에 따라 DataSource 를 분기할 수 있게 됩니다.
'프로젝트 > 프로젝트 관련' 카테고리의 다른 글
JDBC, SQL Mapper(MyBatis), ORM (0) | 2021.12.25 |
---|---|
DTO와 VO는 무엇인가? (0) | 2021.12.23 |
Disk Based Database, In Memory Database (0) | 2021.12.13 |
Sticky Session, Session Clustering, Session Storage (0) | 2021.12.13 |
Block vs Non-Block & Sync vs Async (0) | 2021.12.08 |