지난 블로그 게시글에
관련하여 추가적으로 글을 작성하려고 합니다.
개발을 하다보면 Master(Write)/Slave(Read)로 구성되어 있다해서 Write는 무조건 Master, Read는 무조건 Slave로만 할 수 있는 상황만 생기진 않습니다.
때로는 Write 구현에서 Read 시 Slave가 아닌 Master 트랜잭션을 사용해야 하는 경우도 있습니다.
이유는
- Master와 Slave 간의 데이터 동기화 시간차
때문이죠.
Write 구현에서 Master 트랜잭션으로 데이터 저장
다른 곳에서 해당 데이터를 Slave 트랜잭션에서 조회
이때 해당 데이터가 Slave와 동기화 중임으로 데이터가 없음으로 결과 반환
와 같은 대략적인 상황때문에 어쩔 수 없이(?) Master 트랜잭션에서 Read를 해야 하는 경우가 생깁니다.
Read
Master/Slave Read 클래스 분리
첫번째로 할 수 있는 방법으론 Master/Slave Read를 클래스로 분리하는 방법입니다.
예제 구조를 클래스 다이어그램으로 보자면 아래와 같습니다.
Read시 Master 트랜잭션이 필요한 경우 MemberSuccessExampleMasterReadService
객체를 사용하고 Slave 트랜잭션이 필요한 경우엔 MemberSuccessExampleSlaveReadService
객체를 사용하는 구조입니다.
추상클래스인 MemberSuccessExampleReadServiceAbs
코드를 살펴보자면
/**
* <pre>
* 조회 트랜잭션을 master/slave로 구분짓기 위한 추상 클래스.
*
* 추상 클래스의 기본 트랜잭션은 slave 트랜잭션을 사용한다.
* </pre>
*/
public abstract class MemberSuccessExampleReadServiceAbs implements MemberReadService {
private final MemberRead memberReadMapper;
public MemberSuccessExampleReadServiceAbs(MemberRead memberReadMapper) {
this.memberReadMapper = memberReadMapper;
}
/**
* {@link MemberReadService#findId(String)}
*/
@Override
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
public Member findId(String id) {
return this.memberReadMapper.findId(id);
}
/**
* {@link MemberReadService#findName(String)}
*/
@Override
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
public Member findName(String name) {
return this.memberReadMapper.findName(name);
}
}
와 같이 구현되어 있습니다. Read의 기본 트랜잭션은 Slave로 설계했기 때문에 추상클래스에서 readOnly와 propagation 설정을 통해 Slave 트랜잭션을 기본으로 설정합니다.
그리고 Slave 트랜잭션을 담당하는 MemberSuccessExampleSlaveReadService
코드는 아래와 같습니다.
/**
* <pre>
* {@link MemberReadService} 구현체
* </pre>
*/
@Service
public class MemberSuccessExampleSlaveReadService extends MemberSuccessExampleReadServiceAbs implements MemberReadService {
public MemberSuccessExampleSlaveReadService(MemberRead memberReadMapper) {
super(memberReadMapper);
}
}
추상 클래스에서 Slave 트랜잭션을 기본으로 했기 때문에 해당 클래스는 상속만 받고 그 외 구현은 없습니다.
Master 트랜잭션을 담당하는 MemberSuccessExampleMasterReadService
코드를 살펴보면 아래와 같습니다.
/**
* <pre>
* {@link MemberReadService} 구현체
*
* Master용 Read로 정의한 클래스이기 때문에 추상 클래스에 선언된 slave 트랜잭션 메서드를 Master 트랜잭션으로 재정의한다.
* </pre>
*/
@Service
public class MemberSuccessExampleMasterReadService extends MemberSuccessExampleReadServiceAbs implements MemberReadService {
public MemberSuccessExampleMasterReadService(MemberRead memberReadMapper) {
super(memberReadMapper);
}
/**
* {@link MemberReadService#findId(String)}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Member findId(String id) {
return super.findId(id);
}
/**
* {@link MemberReadService#findName(String)}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Member findName(String name) {
return super.findName(name);
}
}
MemberSuccessExampleMasterReadService
에선 Master 트랜잭션을 위해 각 메서드들을 Override를 하고 @Transactional
를 Master 트랜잭션으로 재정의 합니다.
테스트
먼저 Read Slave 트랜잭션 테스트 결과 입니다.
여기서 this.getMember
메소드는 위 이전 블로그에서 참고하시면 되겠습니다.
@Test
void 회원조회_읽기_쓰기가_다른_클래스에_있는_성공_예제_class() {
final String id = "data_id_1";
Member member = this.getMemberTest(this.memberSuccessExampleSlaveReadService, id);
Assertions.assertEquals(id, member.getId());
}
결과로는 아래와 같이 정상적으로 Slave 트랜잭션을 사용했음을 알 수 있습니다.
[ Test worker] d.s.c.d.p.r.ReplicationRoutingDataSource : isCurrentTransactionReadOnly : master
[ Test worker] c.datasource.DatasourceApplicationTests : Started DatasourceApplicationTests in 1.63 seconds (JVM running for 3.501)
[ Test worker] d.s.c.d.p.r.ReplicationRoutingDataSource : isCurrentTransactionReadOnly : slave
그 다음엔 Read Master 트랜잭션 테스트 결과 입니다.
@Test
void 회원조회_읽기_쓰기가_다른_클래스에_있는_성공_예제_master_class() {
final String id = "data_id_1";
Member member = this.getMemberTest(this.memberSuccessExampleMasterReadService, id);
Assertions.assertEquals(id, member.getId());
}
결과로는 아래와 같이 정상적으로 Master 트랜잭션을 사용했음을 알 수 있습니다.
[ Test worker] d.s.c.d.p.r.ReplicationRoutingDataSource : isCurrentTransactionReadOnly : master
[ Test worker] c.datasource.DatasourceApplicationTests : Started DatasourceApplicationTests in 2.51 seconds (JVM running for 4.684)
[ Test worker] d.s.c.d.p.r.ReplicationRoutingDataSource : isCurrentTransactionReadOnly : master
Factory 패턴 활용
Factory 패턴이라고 했지만 사실 만들어둔 위 2개 클래스들을 조금 편리(?)하게 사용하게끔 하는 사용자 편의 클래스(?)입니다.
위 Master/Slave Read가 필요한 클래스에선 의도치 않게 2개씩 선언 해야하는 불편함(?)이 존재하기 때문에 이를 조금(?)이나마 편하게 사용하기 위한 편의 클래스라고 생각하시면 됩니다.
코드는 아래를 참고하시면 되겠습니다.
/**
* <pre>
* Read 팩토리
* </pre>
*/
@Component
public class MemberSuccessExampleFactory {
private final Map<String, MemberReadService> memberReadServiceMap = new HashMap<>();
public MemberSuccessExampleFactory(MemberReadService memberSuccessExampleSlaveReadService
, MemberReadService memberSuccessExampleMasterReadService) {
// Master 트랜잭션 용 Read
this.memberReadServiceMap.put(MemberSuccessExampleMasterReadService.class.getSimpleName()
, memberSuccessExampleMasterReadService);
// Slave 트랜잭션 용 Read
this.memberReadServiceMap.put(MemberSuccessExampleSlaveReadService.class.getSimpleName()
, memberSuccessExampleSlaveReadService);
}
/**
* <pre>
* Read 트랜잭션 객체 반환.
* </pre>
* @param clazz
* @return
*/
public MemberReadService getInstance(Class<? extends MemberReadService> clazz) {
return Optional.ofNullable(this.memberReadServiceMap.get(clazz.getSimpleName()))
.orElseThrow(() -> new IllegalStateException("지원되지 않는 객체 입니다."));
}
}
MemberSuccessExampleFactory
클래스는 Master/Slave 트랜잭션 클래스를 관리하기 때문에 Master/Slave 트랜잭션 클래스가 필요한 클래스에선 해당 클래스 하나만 선언하면 됩니다. 대략적인 사용방법은 아래 코드를 참고하시면 되겠습니다.
@Component
public class FactoryTest {
private final MemberSuccessExampleFactory memberSuccessExampleFactory;
public FactoryTest(MemberSuccessExampleFactory memberSuccessExampleFactory) {
this.memberSuccessExampleFactory = memberSuccessExampleFactory;
}
/**
* <pre>
* Read Master 트랜잭션 findId 메서드 호출
* </pre>
*/
public Member masterFindId(String id) {
MemberReadService memberMasterReadService = this.memberSuccessExampleFactory.getInstance(MemberSuccessExampleMasterReadService.class);
return memberMasterReadService.findId(id);
}
/**
* <pre>
* Read Slave 트랜잭션 findId 메서드 호출
* </pre>
*/
public Member slaveFindId(String id) {
MemberReadService memberSlaveReadService = this.memberSuccessExampleFactory.getInstance(MemberSuccessExampleSlaveReadService.class);
return memberSlaveReadService.findId(id);
}
}
그러나 세상은 언제나 좋은 점이 있으면 나쁜 점도 있기 마련이라 또 아쉬운 부분은 트랜잭션 객체가 필요할 때 위와 같이 매번 getInstance
메서드를 호출해야 하는 나쁜 점(?)이 있습니다.
테스트
어째든 정상적으로 원하는 결과가 나오는지 테스트 결과를 보겠습니다. 먼저 Master 트랜잭션 테스트 코드를 보자면 아래와 같습니다.
@Test
void 회원조회_읽기_쓰기가_다른_클래스에_있는_성공_예제_factory_master_class() {
final String id = "data_id_1";
Member member = this.getMemberTest(this.memberSuccessExampleFactory.getInstance(MemberSuccessExampleMasterReadService.class), id);
Assertions.assertEquals(id, member.getId());
}
테스트 결과는 아래와 같이 정상적으로 Master 트랜잭션을 사용하였음을 알 수 있습니다.
[ Test worker] d.s.c.d.p.r.ReplicationRoutingDataSource : isCurrentTransactionReadOnly : master
[ Test worker] c.datasource.DatasourceApplicationTests : Started DatasourceApplicationTests in 1.72 seconds (JVM running for 3.901)
[ Test worker] d.s.c.d.p.r.ReplicationRoutingDataSource : isCurrentTransactionReadOnly : master
그 다음에 Slave 트랜잭션 테스트를 보자면 아래와 같습니다.
@Test
void 이름으로_회원조회_읽기_쓰기가_다른_클래스에_있는_성공_예제_factory_slave_class() {
final String name = "data_name_1";
Member member = this.getMemberNameTest(this.memberSuccessExampleFactory.getInstance(MemberSuccessExampleSlaveReadService.class), name);
Assertions.assertEquals(name, member.getName());
}
결과로는 아래와 같이 정상적으로 Slave 트랜잭션을 사용하였음을 알 수 있습니다.
[ Test worker] d.s.c.d.p.r.ReplicationRoutingDataSource : isCurrentTransactionReadOnly : master
[ Test worker] c.datasource.DatasourceApplicationTests : Started DatasourceApplicationTests in 1.629 seconds (JVM running for 3.515)
[ Test worker] d.s.c.d.p.r.ReplicationRoutingDataSource : isCurrentTransactionReadOnly : slave
Decorator 패턴
자 이왕 하는 김에 하나 더 해보겠습니다. 위 Factory 패턴으로 사용하기 편하게(?) 했지만 좀 더 편하게
트랜잭션 메서드가 필요할 때마다
getInstance
메서드를 호출하지 않고선언만으로 사용 할 수 있고
Read 시 기본은 Slave를 사용하고 Slave에 값이 없는 경우 Master를 사용 할 수 있게
Decorator 패턴을 활용한 방법 입니다.
이번 예제는 Master/Slave Read 클래스 분리, Facotry 패턴을 두루두루 사용한 예제라고 생각하시면 되겠습니다.
아래에서 코드를 확인하시면 되겠습니다.
/**
* <pre>
* {@link MemberReadService} 구현체
*
* 읽기 Master/Slave 데코레이터 패턴
* </pre>
*/
@Component
public class MemberSuccessExampleDecoratorService implements MemberReadService {
private final MemberSuccessExampleFactory memberSuccessExampleFactory;
public MemberSuccessExampleDecoratorService(MemberSuccessExampleFactory memberSuccessExampleFactory) {
this.memberSuccessExampleFactory = memberSuccessExampleFactory;
}
/**
* {@link MemberReadService#findId(String)}
*/
@Override
public Member findId(String id) {
return Optional.ofNullable(this.memberSuccessExampleFactory.getInstance(MemberSuccessExampleSlaveReadService.class)
.findId(id))
.orElse(this.memberSuccessExampleFactory.getInstance(MemberSuccessExampleMasterReadService.class)
.findId(id));
}
/**
* {@link MemberReadService#findName(String)}
*/
@Override
public Member findName(String name) {
return Optional.ofNullable(this.memberSuccessExampleFactory.getInstance(MemberSuccessExampleSlaveReadService.class)
.findName(name))
.orElse(this.memberSuccessExampleFactory.getInstance(MemberSuccessExampleMasterReadService.class)
.findName(name));
}
}
Write
이번엔 Write 메소드에서 Read가 필요한 경우 상황에 따라서 Slave 혹은 Master 트랜잭션을 사용하는 예제를 설명하겠습니다.
먼저 테스트를 위해 Write 예제 클래스 다이어그램을 아래를 참고 하시면 되겠습니다.
여기서 예제에서 중요한 추상클래스의 코드를 살펴보자면
/**
* <pre>
* {@link MemberWriteService} 추상클래스
* Write 메소드에서 Read 시 상황에 따라 Master/SLave 트랜잭션을 활용하기 위한 예제 추상클래스
* </pre>
*/
public abstract class MemberSuccessExampleWriteServiceAbs implements MemberWriteService {
private final MemberWrite memberWriteMapper;
private final MemberReadService memberReadService;
public MemberSuccessExampleWriteServiceAbs(MemberWrite memberWriteMapper, MemberReadService memberReadService) {
this.memberWriteMapper = memberWriteMapper;
this.memberReadService = memberReadService;
}
/**
* {@link MemberWriteService#save(Member)}
*/
@Override
@Transactional(rollbackFor = {Exception.class})
public void save(Member member) {
// 해당 메서드 호출 시 MemberReadService 구현체에 따라 Master 혹은 Slave 트랜잭션을 사용하여야 한다.
Member findMember = Optional.ofNullable(this.memberReadService.findId(member.getId()))
.orElseGet(Member::defaultObj);
Optional.ofNullable(findMember.getId())
.filter(""::equals)
.orElseThrow(() -> new IllegalStateException("존재하는 회원 ID 입니다."));
this.memberWriteMapper.save(member);
}
}
와 구현되어 있고 Read 시 필요한 MemberReadService
구현체를 생성자로 받아서 사용하게끔 되어있습니다.
Read Slave 트랜잭션 사용
Write 메소드에서 Read시 Slave 트랜잭션을 사용하는 예제 입니다.
/**
* {@link MemberWriteService} 추상 클래스
*
* 정상적으로 읽기는 slave로 저장/수정/삭제는 master 트랜잭션을 사용하는 예제 클래스
* 해당 구현체는 읽기 시 slave를 사용하기 위한 용도
*/
@Service
public class MemberSuccessExampleSlaveReadWriteService extends MemberSuccessExampleWriteServiceAbs implements MemberWriteService {
public MemberSuccessExampleSlaveReadWriteService(MemberWrite memberWriteMapper, MemberSuccessExampleFactory memberSuccessExampleFactory) {
super(memberWriteMapper, memberSuccessExampleFactory.getInstance(MemberSuccessExampleSlaveReadService.class));
}
}
구현은 위와 같으며 추상클래스에서 필요한 MemberReadService
구현체를 MemberSuccessExampleSlaveReadService
객체로 설정합니다.
테스트
테스트코드는 아래를 참고하시면 되겠습니다.
@Test
void 회원가입성공_읽기_쓰기가_다른_클래스에_있는_성공_예제_class() {
Member member = new Member("data_id_4", "data_name_1", 40);
this.saveMemberTest(this.memberSuccessExampleSlaveReadWriteService, member);
}
아래 결과를 보면 정상적으로 Write 메소드에서 Read시 Slave 트랜잭션을 사용한것을 확인하실 수 있습니다.
[ Test worker] d.s.c.d.p.r.ReplicationRoutingDataSource : isCurrentTransactionReadOnly : master
[ Test worker] c.datasource.DatasourceApplicationTests : Started DatasourceApplicationTests in 1.525 seconds (JVM running for 3.177)
[ Test worker] d.s.c.d.p.r.ReplicationRoutingDataSource : isCurrentTransactionReadOnly : slave
Read Master 트랜잭션 사용
Write 메소드에서 Read시 Master 트랜잭션을 사용하는 예제 입니다.
/**
* {@link MemberWriteService} 추상 클래스
*
* 정상적으로 읽기는 slave로 저장/수정/삭제는 master 트랜잭션을 사용하는 예제 클래스
* 해당 구현체는 읽기 시 master를 사용하기 위한 용도
*/
@Service
public class MemberSuccessExampleMasterReadWriteService extends MemberSuccessExampleWriteServiceAbs implements MemberWriteService {
public MemberSuccessExampleMasterReadWriteService(MemberWrite memberWriteMapper, MemberSuccessExampleFactory memberSuccessExampleFactory) {
super(memberWriteMapper, memberSuccessExampleFactory.getInstance(MemberSuccessExampleMasterReadService.class));
}
}
구현은 위와 같으며 추상클래스에서 필요한 MemberReadService
구현체를 MemberSuccessExampleMasterReadService
객체로 설정합니다.
테스트
테스트코드는 아래를 참고하시면 되겠습니다.
@Test
void 회원가입성공_읽기_쓰기가_다른_클래스에_있는_성공_예제_master_class() {
Member member = new Member("data_id_4", "data_name_1", 40);
this.saveMemberTest(this.memberSuccessExampleMasterReadWriteService, member);
}
아래 결과를 보면 정상적으로 Write 메소드에서 Read시 Master 트랜잭션을 사용한것을 확인 하실 수 있습니다.
[ Test worker] d.s.c.d.p.r.ReplicationRoutingDataSource : isCurrentTransactionReadOnly : master
[ Test worker] c.datasource.DatasourceApplicationTests : Started DatasourceApplicationTests in 1.536 seconds (JVM running for 3.699)
[ Test worker] d.s.c.d.p.r.ReplicationRoutingDataSource : isCurrentTransactionReadOnly : master
Decorator 패턴 트랜잭션 사용
Write 메소드에서 Read시 Slave 트랜잭션에 데이터가 없는 경우 Master 트랜잭션에서 데이터를 가지고 오는 예제입니다.
/**
* {@link MemberWriteService} 추상 클래스
*
* 정상적으로 읽기는 slave로 저장/수정/삭제는 master 트랜잭션을 사용하는 예제 클래스
* 해당 구현체는 Decorator 패턴 예제를 위한 용도
*/
@Service
public class MemberSuccessExampleDecoratorWriteService extends MemberSuccessExampleWriteServiceAbs implements MemberWriteService {
public MemberSuccessExampleDecoratorWriteService(MemberWrite memberWriteMapper, MemberReadService memberSuccessExampleDecoratorService) {
super(memberWriteMapper, memberSuccessExampleDecoratorService);
}
}
테스트
테스트코드는 아래를 참고하시면 되겠습니다.
@Test
void 회원가입성공_읽기_쓰기가_다른_클래스에_있는_성공_예제_decorator() {
Member member = new Member("data_id_4", "data_name_1", 40);
this.saveMemberTest(this.memberSuccessExampleDecoratorWriteService, member);
}
해당 예제의 테스트 결과는 별도로 작성하진 않겠습니다. 본 예제 프로젝트에서 테스트가 가능한 코드를 작성하려니 불필요한 코드가 생겨 작성하지 않았습니다.
그래도 확인해보고 싶은 분들은 MemberSuccessExampleDecoratorService
클래스의 findId
메소드를 아래와 같이 변경하신 다음 위 테스트 코드를 실행 시키면 되겠습니다.
@Override
public Member findId(String id) {
// 정상 케이스라면 Read Slave 트랜잭션에서 findId 메서드를 호출해야 하지만 강제로 Master 트랜잭션을 사용해야함으로 선언만 함.
MemberReadService memberReadService = this.memberSuccessExampleFactory.getInstance(MemberSuccessExampleSlaveReadService.class);
/*
Slave 트랜잭션에 데이터가 없다는 가정을 위해 null 설정
정상은 Optional.ofNullable(memberReadService.findId(id))
*/
Optional<Member> optionalMember = Optional.ofNullable(null);
if(optionalMember.isEmpty()) {
return this.memberSuccessExampleFactory.getInstance(MemberSuccessExampleMasterReadService.class).findId(name);
}
return optionalMember.get();
}
마치며
본 예제 코드들의 프로젝트는 GitHub - sungwookkim/spring-multi-datasource-service at multi-datasource-service-master-read에서 확인 하실 수 있습니다.
지금까지 길다면 길고 짧다면 짧은 글을 끝까지 읽어주신 분들께 감사 말씀 드리며 하시는 모든 개발이 성공적으로 끝나길 바라고 하시는 모든 일에 축복이 가득하길 기도 드리겠습니다. 감사합니다.