해당 아티클의 1부는
에서 확인하실 수 있습니다.
Service
서비스 영역 코드는 테스트 코드이기 때문에 별도의 설명 없이 코드만 작성하도록 하겠습니다.
단일 트랜잭션
SingleServiceImpl
Mybatis용 서비스 테스트 코드 입니다.
package com.datasource.service.single;
import com.datasource.repo.mybatis.single.singleA.SingleAMapper;
import com.datasource.repo.mybatis.single.singleB.SingleBMapper;
import com.datasource.spring.config.annotaion.SingleATransactional;
import com.datasource.spring.config.annotaion.SingleBTransactional;
import org.springframework.stereotype.Service;
/**
* <pre>
* Mybatis를 활용한 JTA 트랜잭션이 아닌 단일 트랜잭션을 확인하기 위핸 테스트 클래스.
* </pre>
*/
@Service
public class SingleServiceImpl {
private final SingleAMapper singleAMapper;
private final SingleBMapper singleBMapper;
public SingleServiceImpl(SingleAMapper singleAMapper, SingleBMapper singleBMapper) {
this.singleAMapper = singleAMapper;
this.singleBMapper = singleBMapper;
}
/**********************/
/*Single A 트랜잭션 영역*/
/**********************/
/**
* <pre>
* SingleA 단일 트랜잭션 정상 저장을 확인하기 위한 메서드.
* </pre>
*/
@SingleATransactional(rollbackFor = {Exception.class})
public long singleASaveTest(String testText) {
return this.singleAMapper.saveTest(testText);
}
/**
* <pre>
* SingleA 단일 트랜잭션 롤백을 확인하기 위한 메서드.
* </pre>
*/
@SingleATransactional(rollbackFor = {Exception.class})
public long singleASaveRollbackTest(String testText) {
this.singleAMapper.saveTest(testText);
throw new RuntimeException("Single A Rollback Test");
}
/**
* <pre>
* SingleA 단일 트랜잭션 정상 조회를 확인하기 위한 메서드.
* </pre>
*/
@SingleATransactional
public String singleAFindTestText(String testText) {
return this.singleAMapper.findTestText(testText);
}
/**********************/
/*Single B 트랜잭션 영역*/
/**********************/
/**
* <pre>
* SingleB 단일 트랜잭션 정상 저장을 확인하기 위한 메서드.
* </pre>
*/
@SingleBTransactional(rollbackFor = {Exception.class})
public long singleBSaveTest(String testText) {
return this.singleBMapper.saveTest(testText);
}
/**
* <pre>
* SingleB 단일 트랜잭션 롤백을 확인하기 위한 메서드.
* </pre>
*/
@SingleBTransactional(rollbackFor = {Exception.class})
public long singleBSaveRollbackTest(String testText) {
this.singleBMapper.saveTest(testText);
throw new RuntimeException("Single B Rollback Test");
}
/**
* <pre>
* SingleB 단일 트랜잭션 정상 조회를 확인하기 위한 메서드.
* </pre>
*/
@SingleBTransactional
public String singleBFindTestText(String testText) {
return this.singleBMapper.findTestText(testText);
}
}
SingleJpaServiceImpl
JPA용 서비스 테스트 코드 입니다.
package com.datasource.service.single;
import com.datasource.domain.singleA.SingleAJpa;
import com.datasource.domain.singleB.SingleBJpa;
import com.datasource.repo.jpa.single.singleA.SingleARepository;
import com.datasource.repo.jpa.single.singleB.SingleBRepository;
import com.datasource.spring.config.annotaion.SingleAJpaTransactional;
import com.datasource.spring.config.annotaion.SingleBJpaTransactional;
import org.springframework.stereotype.Service;
import java.util.Optional;
/**
* <pre>
* JPA를 활용한 JTA 트랜잭션이 아닌 단일 트랜잭션을 확인하기 위핸 테스트 클래스.
* </pre>
*/
@Service
public class SingleJpaServiceImpl {
private final SingleARepository singleARepository;
private final SingleBRepository singleBRepository;
public SingleJpaServiceImpl(SingleARepository singleARepository, SingleBRepository singleBRepository) {
this.singleARepository = singleARepository;
this.singleBRepository = singleBRepository;
}
/**********************/
/*Single A 트랜잭션 영역*/
/**********************/
/**
* <pre>
* SingleA 단일 트랜잭션 정상 저장을 확인하기 위한 메서드.
* </pre>
*/
@SingleAJpaTransactional(rollbackFor = {Exception.class})
public void singleASaveTest(SingleAJpa singleAJpa) {
this.singleARepository.save(singleAJpa);
}
/**
* <pre>
* SingleA 단일 트랜잭션 정상 롤백을 확인하기 위한 메서드.
* </pre>
*/
@SingleAJpaTransactional(rollbackFor = {Exception.class})
public void singleARollbackTest(SingleAJpa singleAJpa) {
this.singleARepository.save(singleAJpa);
throw new RuntimeException("Single B Rollback Test");
}
/**
* <pre>
* SingleA 단일 트랜잭션 정상 조회를 확인하기 위한 메서드.
* </pre>
*/
@SingleAJpaTransactional
public Optional<SingleAJpa> singleAFindTestText(Long seq) {
return this.singleARepository.findById(seq);
}
/**********************/
/*Single B 트랜잭션 영역*/
/**********************/
/**
* <pre>
* SingleB 단일 트랜잭션 정상 저장을 확인하기 위한 메서드.
* </pre>
*/
@SingleBJpaTransactional(rollbackFor = {Exception.class})
public void singleBSaveTest(SingleBJpa singleBJpa) {
this.singleBRepository.save(singleBJpa);
}
/**
* <pre>
* SingleB 단일 트랜잭션 정상 롤백을 확인하기 위한 메서드.
* </pre>
*/
@SingleBJpaTransactional(rollbackFor = {Exception.class})
public void singleBRollbackTest(SingleBJpa singleBJpa) {
this.singleBRepository.save(singleBJpa);
throw new RuntimeException("Single B Rollback Test");
}
/**
* <pre>
* SingleB 단일 트랜잭션 정상 조회를 확인하기 위한 메서드.
* </pre>
*/
@SingleBJpaTransactional
public Optional<SingleBJpa> singleBFindTestText(Long seq) {
return this.singleBRepository.findById(seq);
}
}
JTA 트랜잭션
JtaServiceImpl
Mybatis용 JTA 서비스 테스트 코드 입니다.
package com.datasource.service.jta;
import com.datasource.repo.mybatis.jta.singleA.JtaSingleAMapper;
import com.datasource.repo.mybatis.jta.singleB.JtaSingleBMapper;
import com.datasource.spring.config.annotaion.JtaTransactional;
import org.springframework.stereotype.Service;
/**
* <pre>
* Mybatis JTA 트랜잭션 확인을 위한 테스트 클래스.
* </pre>
*/
@Service
public class JtaServiceImpl {
private final JtaSingleAMapper jtaSingleAMapper;
private final JtaSingleBMapper jtaSingleBMapper;
public JtaServiceImpl(JtaSingleAMapper jtaSingleAMapper, JtaSingleBMapper jtaSingleBMapper) {
this.jtaSingleAMapper = jtaSingleAMapper;
this.jtaSingleBMapper = jtaSingleBMapper;
}
/**
* <pre>
* 이기종 간(SingleA, SingelB)의 정상 트랜잭션을 확인 하기 위한 메서드.
* </pre>
*/
@JtaTransactional(rollbackFor = {Exception.class})
public long saveTest(String testText) {
return this.jtaSingleAMapper.saveTest(testText) + this.jtaSingleBMapper.saveTest(testText);
}
/**
* <pre>
* 이기 간(SingleA, SingleB)의 트랜잭션 롤백을 확인 하기 위한 메서드.
* </pre>
*/
@JtaTransactional(rollbackFor = {Exception.class})
public void saveRollbackTest(String testText) {
this.jtaSingleAMapper.saveTest(testText);
this.jtaSingleBMapper.saveTest(testText);
if(true) {
throw new RuntimeException("Rollback Test Exception");
}
}
}
JpaJtaServiceImpl
JPA용 JTA 서비스 테스트 코드 입니다.
package com.datasource.service.jta;
import com.datasource.domain.singleA.SingleAJpa;
import com.datasource.domain.singleB.SingleBJpa;
import com.datasource.repo.jpa.jta.singleA.JtaSingleARepository;
import com.datasource.repo.jpa.jta.singleB.JtaSingleBRepository;
import com.datasource.spring.config.annotaion.JtaTransactional;
import org.springframework.stereotype.Service;
/**
* <pre>
* JPA JTA 트랜잭션 확인을 위한 테스트 클래스.
* </pre>
*/
@Service
public class JpaJtaServiceImpl {
private final JtaSingleARepository jtaSingleARepository;
private final JtaSingleBRepository jtaSingleBRepository;
public JpaJtaServiceImpl(JtaSingleARepository jtaSingleARepository, JtaSingleBRepository jtaSingleBRepository) {
this.jtaSingleARepository = jtaSingleARepository;
this.jtaSingleBRepository = jtaSingleBRepository;
}
/**
* <pre>
* 이기종 간(SingleA, SingelB)의 정상 트랜잭션을 확인 하기 위한 메서드.
* </pre>
*/
@JtaTransactional(rollbackFor = {Exception.class})
public void saveTest(SingleAJpa singleAJpa, SingleBJpa singleBJpa) {
this.jtaSingleARepository.save(singleAJpa);
this.jtaSingleBRepository.save(singleBJpa);
}
/**
* <pre>
* 이기종 간(SingleA, SingleB)의 트랜잭션 롤백을 확인 하기 위한 메서드.
* </pre>
*/
@JtaTransactional(rollbackFor = {Exception.class})
public void saveRollbackTest(SingleAJpa singleAJpa, SingleBJpa singleBJpa) {
this.jtaSingleARepository.save(singleAJpa);
this.jtaSingleBRepository.save(singleBJpa);
if(true) {
throw new RuntimeException("Rollback Test Exception");
}
}
}
테스트
JTA 로그를 면밀하게 살펴 보기 위해 logback 설정을 아래와 같이 하였습니다. 로그 설정 중 CompositeTransactionImp
클래스 부분이 JTA 트랜잭션을 관리하는 클래스 입니다. 실제로 Commit과 Rollback 같은 행위는 다른 클래스들이 담당하지만 최종적으론 해당 클래스를 통해 Commit과 Rollback 등이 수행 됩니다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="LogToConsole" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread{10}] %logger{0}\\(%line\\) - %msg %n </pattern>
</encoder>
</appender>
<logger name="org.springframework.transaction.jta.JtaTransactionManager" level="debug"/>
<logger name="com.atomikos.jdbc.AtomikosDataSourceBean" level="debug"/>
<logger name="com.atomikos.jdbc.AbstractDataSourceBean" level="debug"/>
<logger name="com.atomikos.jdbc.AtomikosConnectionProxy" level="debug"/>
<logger name="com.atomikos.icatch.imp.CompositeTransactionImp" level="debug"/>
<root level="info">
<appender-ref ref="LogToConsole"/>
</root>
</configuration>
그리고 예제 프로젝트 내에 jta.properties
파일이 존재하는데 해당 파일이 Atomikos의 설정 파일 입니다. 설정 파일은 프로젝트의 최상위 경로에 위치해야 하며 Atomikos 설정 파일이 없으면 기본으로 라이브러리에 내장되어 있는 transactions-defaults.properties
파일을 참조하게 됩니다.
Atomikos는 아래 파일명의 설정 파일들을 찾게 되고 각 설정 파일마다 적용되는 우선 순위가 존재합니다. 우선 순위에 따라 설정 값들이 추가 되거나 덮어쓰기가 됩니다.
transactions-defaults.properties
라이브러리에 있는 기본 설정 파일transactions.properties
jta.properties
JVM (System) Properties
Java 실행 시 -Dcom.atomikos.icatch.file 매개변수에 위치한 properties 파일
Atomikos의 AssemblerImp
구현체를 확인해보면 아래와 같이 구현되어 있습니다.
public class AssemblerImp implements Assembler {
private static final String DEFAULT_PROPERTIES_FILE_NAME = "transactions-defaults.properties";
private static final String JTA_PROPERTIES_FILE_NAME = "jta.properties";
private static final String TRANSACTIONS_PROPERTIES_FILE_NAME = "transactions.properties";
.
.
.
@Override
public ConfigProperties initializeProperties() {
Properties defaults = new Properties();
// 첫 번째로 라이브러리 내에 존재하는 transactions-defaults.properties 파일을 로드.
loadPropertiesFromClasspath(defaults, DEFAULT_PROPERTIES_FILE_NAME);
Properties transactionsProperties = new Properties(defaults);
// 두 번째로 프로젝트 최상위 경로의 transactions.properties 파일을 로드해서 설정을 추가하거나 덮어쓰기.
loadPropertiesFromClasspath(transactionsProperties, TRANSACTIONS_PROPERTIES_FILE_NAME);
Properties jtaProperties = new Properties(transactionsProperties);
// 세 번째로 프로젝트 최상위 경로의 jta.properties 파일을 로드해서 설정을 추가하거나 덮어쓰기.
loadPropertiesFromClasspath(jtaProperties, JTA_PROPERTIES_FILE_NAME);
Properties customProperties = new Properties(jtaProperties);
// 네 번째로 java 실행 시 -Dcom.atomikos.icatch.file 매개 변수에 위치한 properties 파일을 로드해서 설정을 추가하거나 덮어쓰기.
loadPropertiesFromCustomFilePath(customProperties);
Properties finalProperties = new Properties(customProperties);
ConfigProperties configProperties = new ConfigProperties(finalProperties);
checkRegistration(configProperties);
return configProperties;
}
.
.
.
}
Mybatis
단일 트랜잭션
정상 저장 테스트
단일 트랜잭션으로 되어 있는 정상 저장 테스트 입니다.
@Test
@DisplayName("단일 트랜잭션 정상 저장 테스트")
void single_datasource_insert_test() {
String saveAndFindValue = "single_test_value";
long singleASeq = this.singleServiceImpl.singleASaveTest(saveAndFindValue);
long singleBSeq = this.singleServiceImpl.singleBSaveTest(saveAndFindValue);
logger.info("singleASeq : {}, singleBSeq : {}", singleASeq, singleBSeq);
Assertions.assertTrue(singleASeq > 0 && singleBSeq > 0);
}
결과
큰 문제 없이 정상 저장 되었음을 알 수 있습니다.
JtaApplicationTests(71) - singleASeq : 1, singleBSeq : 1
정상 롤백 테스트
JTA를 사용한 트랜잭션이 아니기 때문에 각 Single A, B 트랜잭션들이 정상적으로 롤백이 되는지를 확인하였습니다.
@Test
@DisplayName("단일 트랜잭션 정상 롤백 테스트")
void single_datasource_rollback_test() {
String saveAndFindValue = "single_rollback_test_value";
Assertions.assertThrowsExactly(RuntimeException.class, () -> {
this.singleServiceImpl.singleASaveRollbackTest(saveAndFindValue);
});
Assertions.assertThrowsExactly(RuntimeException.class, () -> {
this.singleServiceImpl.singleBSaveRollbackTest(saveAndFindValue);
});
Assertions.assertTrue(null == this.singleServiceImpl.singleAFindTestText(saveAndFindValue)
&& null == this.singleServiceImpl.singleBFindTestText(saveAndFindValue));
}
결과
별도 로그 출력은 없습니다.
JTA 트랜잭션
정상 저장 테스트
JTA 트랜잭션을 사용해서 Single A, B에 정상으로 저장되는지 여부를 확인하는 테스트 입니다.
@Test
@DisplayName("JTA 트랜잭션 정상 저장 테스트")
void jta_datasource_insert_test() {
String saveAndFindValue = "jta_test_value";
logger.info("jta insert count : {}", this.jtaServiceImpl.saveTest(saveAndFindValue));
// JTA 트랜잭션을 통해 정상적으로 저장되었는지 확인하기 위해 JTA 트랜잭션이 아닌 일반 트랜잭션으로 조회.
Assertions.assertTrue(saveAndFindValue.equals(this.singleServiceImpl.singleAFindTestText(saveAndFindValue))
&& saveAndFindValue.equals(this.singleServiceImpl.singleBFindTestText(saveAndFindValue)));
}
결과
Single A, B에 정상적으로 저장 되었음을 확인할 수 있습니다.
JtaTransactionManager(370) - Creating new transaction with name [com.datasource.service.jta.JtaServiceImpl.saveTest]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'jtaSingleTransactionManager',-java.lang.Exception
TransactionServiceImp(24) - Attempt to create a transaction with a timeout that exceeds maximum - truncating to: 300000
AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleADataSource': getConnection()...
AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleADataSource': init...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: calling getAutoCommit...
CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535303930333030303031:3139322E3136382E312E3230372E746D31 ) for transaction 192.168.1.207.tm169812355090300001
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@4ba4e4c1 ) for transaction 192.168.1.207.tm169812355090300001
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: calling prepareStatement(insert into test(
test_text
) values (
?
))...
AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleBDataSource': getConnection()...
AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleBDataSource': init...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@51297528: calling getAutoCommit...
CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535303930333030303031:3139322E3136382E312E3230372E746D32 ) for transaction 192.168.1.207.tm169812355090300001
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@4ba4e4c1 ) for transaction 192.168.1.207.tm169812355090300001
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@51297528: calling prepareStatement(insert into test(
test_text
) values (
?
))...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: close()...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@51297528: close()...
JtaTransactionManager(740) - Initiating transaction commit
CompositeTransactionImp(32) - commit() done (by application) of transaction 192.168.1.207.tm169812355090300001
JtaApplicationTests(98) - jta insert count : 2
위 로그를 보면 아래와 같은 형태로 각 DB 작업마다 수행 했음을 알 수 있습니다.
AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleADataSource': getConnection()...
AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleADataSource': init...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: calling getAutoCommit...
CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535303930333030303031:3139322E3136382E312E3230372E746D31 ) for transaction 192.168.1.207.tm169812355090300001
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@4ba4e4c1 ) for transaction 192.168.1.207.tm169812355090300001
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: calling prepareStatement(insert into test(
test_text
) values (
?
))...
그리고 마지막으로 아래와 같이 성공적으로 트랜잭션이 Commit 되었음을 확인할 수 있습니다.
JtaTransactionManager(740) - Initiating transaction commit
CompositeTransactionImp(32) - commit() done (by application) of transaction 192.168.1.207.tm169812355090300001
정상 롤백 테스트
JTA 트랜잭션을 사용해서 Single A, B에 정상으로 롤백이 되었는지 확인하는 테스트 입니다.
@Test
@DisplayName("JTA 트랜잭션 정상 롤백 테스트")
void jta_rollback_test() {
String saveAndFindValue = "jta_rollback_test_value";
Assertions.assertThrowsExactly(RuntimeException.class, () -> {
this.jtaServiceImpl.saveRollbackTest(saveAndFindValue);
});
Assertions.assertTrue(null == this.singleServiceImpl.singleAFindTestText(saveAndFindValue)
&& null == this.singleServiceImpl.singleBFindTestText(saveAndFindValue));
}
결과
Single A, B에 정상적으로 롤백이 되었음을 확인할 수 있습니다.
JtaTransactionManager(370) - Creating new transaction with name [com.datasource.service.jta.JtaServiceImpl.saveRollbackTest]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'jtaSingleTransactionManager',-java.lang.Exception
TransactionServiceImp(24) - Attempt to create a transaction with a timeout that exceeds maximum - truncating to: 300000
AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleADataSource': getConnection()...
AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleADataSource': init...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: calling getAutoCommit...
CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535313634323030303032:3139322E3136382E312E3230372E746D33 ) for transaction 192.168.1.207.tm169812355164200002
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@7a29e18b ) for transaction 192.168.1.207.tm169812355164200002
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: calling prepareStatement(insert into test(
test_text
) values (
?
))...
AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleBDataSource': getConnection()...
AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleBDataSource': init...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@51297528: calling getAutoCommit...
CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535313634323030303032:3139322E3136382E312E3230372E746D34 ) for transaction 192.168.1.207.tm169812355164200002
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@7a29e18b ) for transaction 192.168.1.207.tm169812355164200002
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@51297528: calling prepareStatement(insert into test(
test_text
) values (
?
))...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@beabd6b: close()...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@51297528: close()...
JtaTransactionManager(833) - Initiating transaction rollback
CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812355164200002
CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812355164200002
아래를 보면 정상적으로 롤백이 수행되었음 알 수 있습니다.
JtaTransactionManager(833) - Initiating transaction rollback
CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812355164200002
CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812355164200002
여기서 잠시 JPA 테스트를 확인하기 전에 추가적으로 설명 드릴 부분이 있는데 Atomikos는 JTA 트랜잭션 처리를 위해 트랜잭션 로그 파일을 생성합니다. 이 트랜잭션 로그 파일은 개발자가 제어할 수 없는 부분이며 해당 파일에는 트랜잭션 로그와 기타 트랜잭션 관리에 필요한 정보가 저장됩니다. 이 트랜잭션 로그 파일를 가지고 트랜잭션 롤백 및 복구와 같은 중요한 작업을 수행하게 됩니다.
다시 위 테스트 로그 중 트랜잭션 로그 파일과 관련된 로그를 보면 192.168.1.207.tm169812355164200002
와 같은 형태로 각 DB 작업이 어떤 트랜잭션에 묶여있는지 확인할 수 있습니다.
-- Single A DB
addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535313634323030303032:3139322E3136382E312E3230372E746D33 ) for transaction 192.168.1.207.tm169812355164200002
registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@7a29e18b ) for transaction 192.168.1.207.tm169812355164200002
-- Single B DB
addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132333535313634323030303032:3139322E3136382E312E3230372E746D34 ) for transaction 192.168.1.207.tm169812355164200002
registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@7a29e18b ) for transaction 192.168.1.207.tm169812355164200002
그리고 해당 DB 작업들에 트랜잭션 결과도 192.168.1.207.tm169812355164200002
와 같은 형태로 어떤게 수행 되었는지 확인할 수 있습니다.
rollback() done of transaction 192.168.1.207.tm169812355164200002
트랜잭션 로그 파일의 내용을 확인해보면 아래와 같은 로그 정보가 포함되어 있습니다.(해당 부분은 Spring 2.7.16 버전이며 3.1.4 내용이 달라집니다.)
-- Rollback인 경우
{
"id": "192.168.1.207.tm169830186293600001",
"wasCommitted": false,
"participants": [
{
"uri": "192.168.1.207.tm1",
"state": "TERMINATED",
"expires": 1698302163038,
"resourceName": "jtaSingleADataSource"
},
{
"uri": "192.168.1.207.tm2",
"state": "TERMINATED",
"expires": 1698302163038,
"resourceName": "jtaSingleBDataSource"
}
]
}
-- Commit인 경우
{
"id": "192.168.1.207.tm169830186308000002",
"wasCommitted": true,
"participants": [
{
"uri": "192.168.1.207.tm3",
"state": "COMMITTING",
"expires": 1698302163106,
"resourceName": "jtaSingleADataSource"
},
{
"uri": "192.168.1.207.tm4",
"state": "COMMITTING",
"expires": 1698302163106,
"resourceName": "jtaSingleBDataSource"
}
]
}
JPA
단일 트랜잭션
정상 저장 테스트
JPA를 활용한 단일 트랜잭션 정상 저장 테스트 입니다.
@Test
@DisplayName("단일 트랜잭션 정상 저장 테스트")
void single_jpa_datasource_insert_test() {
String saveAndFindValue = "single_test_vale";
SingleAJpa singleAJpa = new SingleAJpa(saveAndFindValue);
SingleBJpa singleBJpa = new SingleBJpa(saveAndFindValue);
this.singleJpaServiceImpl.singleASaveTest(singleAJpa);
this.singleJpaServiceImpl.singleBSaveTest(singleBJpa);
long singleASeq = singleAJpa.getSeq();
long singleBSeq = singleBJpa.getSeq();
logger.info("singleASeq : {}, singleBSeq : {}", singleASeq, singleBSeq);
Assertions.assertTrue(singleASeq > 0 && singleBSeq > 0);
}
결과
정상으로 저장되었음을 확인할 수 있습니다.
Hibernate:
insert
into
single_a_jpa
(test_text)
values
(?)
Hibernate:
insert
into
single_b_jpa
(test_text)
values
(?)
JtaJpaApplicationTests(38) - singleASeq : 3, singleBSeq : 3
정상 롤백 테스트
롤백 테스트도 위 Mybatis 롤백 테스트와 동일하게 각 단일 트랜잭션에 대한 롤백 테스트를 하였습니다.
@Test
@DisplayName("단일 트랜잭션 정상 롤백 테스트")
void single_jpa_datasource_rollback_test() {
String saveAndFindValue = "single_test_vale";
SingleAJpa singleAJpa = new SingleAJpa(saveAndFindValue);
SingleBJpa singleBJpa = new SingleBJpa(saveAndFindValue);
Assertions.assertThrowsExactly(RuntimeException.class, () -> this.singleJpaServiceImpl.singleARollbackTest(singleAJpa));
Assertions.assertThrowsExactly(RuntimeException.class, () -> this.singleJpaServiceImpl.singleBRollbackTest(singleBJpa));
long singleASeq = singleAJpa.getSeq();
long singleBSeq = singleBJpa.getSeq();
Assertions.assertTrue(this.singleJpaServiceImpl.singleAFindTestText(singleASeq).isEmpty()
&& this.singleJpaServiceImpl.singleBFindTestText(singleBSeq).isEmpty());
}
결과
각 단일 트랜잭션 롤백에 대한 테스트도 정상임을 알 수 있습니다.
Hibernate:
insert
into
single_a_jpa
(test_text)
values
(?)
Hibernate:
insert
into
single_b_jpa
(test_text)
values
(?)
Hibernate:
select
singleajpa0_.seq as seq1_0_0_,
singleajpa0_.test_text as test_tex2_0_0_
from
single_a_jpa singleajpa0_
where
singleajpa0_.seq=?
Hibernate:
select
singlebjpa0_.seq as seq1_0_0_,
singlebjpa0_.test_text as test_tex2_0_0_
from
single_b_jpa singlebjpa0_
where
singlebjpa0_.seq=?
JTA 트랜잭션
정상 저장 테스트
JTA 트랜잭션을 사용한 저장 테스트 입니다.
@Test
@DisplayName("JTA 트랜잭션 정상 저장 테스트")
void jta_jpa_datasource_insert_test() {
String saveAndFindValue = "jta_test_value";
SingleAJpa singleAJpa = new SingleAJpa(saveAndFindValue);
SingleBJpa singleBJpa = new SingleBJpa(saveAndFindValue);
this.jpaJtaServiceImpl.saveTest(singleAJpa, singleBJpa);
long singleASeq = singleAJpa.getSeq();
long singleBSeq = singleBJpa.getSeq();
logger.info("singleASeq : {}, singleBSeq : {}", singleASeq, singleBSeq);
// JTA 트랜잭션을 통해 정상적으로 저장되었는지 확인하기 위해 JTA 트랜잭션이 아닌 일반 트랜잭션으로 조회.
Assertions.assertTrue(saveAndFindValue.equals(this.singleJpaServiceImpl.singleAFindTestText(singleASeq).get().getTestText())
&& saveAndFindValue.equals(this.singleJpaServiceImpl.singleBFindTestText(singleBSeq).get().getTestText()));
}
결과
CompositeTransactionImp(32) - commit() done (by application) of transaction 192.168.1.207.tm169812958131600002
출력으로 보아 JTA 트랜잭션을 사용해서 Single A, B 둘 다 정상 Commit을 확인할 수 있습니다.
JtaTransactionManager(370) - Creating new transaction with name [com.datasource.service.jta.JpaJtaServiceImpl.saveTest]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'jtaSingleTransactionManager',-java.lang.Exception
TransactionServiceImp(24) - Attempt to create a transaction with a timeout that exceeds maximum - truncating to: 300000
JtaTransactionManager(470) - Participating in existing transaction
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.icatch.jta.Sync2Sync@796e2187 ) for transaction 192.168.1.207.tm169812958131600002
AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleADataSource': getConnection()...
AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleADataSource': init...
CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132393538313331363030303032:3139322E3136382E312E3230372E746D33 ) for transaction 192.168.1.207.tm169812958131600002
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@b5ad3f3e ) for transaction 192.168.1.207.tm169812958131600002
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: calling prepareStatement(insert into single_a_jpa (test_text) values (?),1)...
JtaTransactionManager(470) - Participating in existing transaction
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.icatch.jta.Sync2Sync@59db8216 ) for transaction 192.168.1.207.tm169812958131600002
AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleBDataSource': getConnection()...
AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleBDataSource': init...
CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132393538313331363030303032:3139322E3136382E312E3230372E746D34 ) for transaction 192.168.1.207.tm169812958131600002
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@b5ad3f3e ) for transaction 192.168.1.207.tm169812958131600002
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: calling prepareStatement(insert into single_b_jpa (test_text) values (?),1)...
JtaTransactionManager(740) - Initiating transaction commit
CompositeTransactionImp(32) - commit() done (by application) of transaction 192.168.1.207.tm169812958131600002
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: isClosed()...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: calling getWarnings...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: calling clearWarnings...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: close()...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: isClosed()...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: calling getWarnings...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: calling clearWarnings...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: close()...
JtaJpaApplicationTests(73) - singleASeq : 4, singleBSeq : 4
Hibernate:
select
singleajpa0_.seq as seq1_0_0_,
singleajpa0_.test_text as test_tex2_0_0_
from
single_a_jpa singleajpa0_
where
singleajpa0_.seq=?
Hibernate:
select
singlebjpa0_.seq as seq1_0_0_,
singlebjpa0_.test_text as test_tex2_0_0_
from
single_b_jpa singlebjpa0_
where
singlebjpa0_.seq=?
정상 롤백 테스트
JTA 트랜잭션을 사용한 롤백 테스트 입니다.
@Test
@DisplayName("JTA 트랜잭션 정상 롤백 테스트")
void jta_jpa_rollback_test() {
String saveAndFindValue = "jta_rollback_test_value";
Assertions.assertThrowsExactly(RuntimeException.class, () -> {
SingleAJpa singleAJpa = new SingleAJpa(saveAndFindValue);
SingleBJpa singleBJpa = new SingleBJpa(saveAndFindValue);
this.jpaJtaServiceImpl.saveRollbackTest(singleAJpa, singleBJpa);
});
}
결과
마찬가지로 실행 로그에 CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812958119800001
출력된 것으로 롤백 되었음을 확인할 수 있습니다.
JtaTransactionManager(370) - Creating new transaction with name [com.datasource.service.jta.JpaJtaServiceImpl.saveRollbackTest]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'jtaSingleTransactionManager',-java.lang.Exception
TransactionServiceImp(24) - Attempt to create a transaction with a timeout that exceeds maximum - truncating to: 300000
JtaTransactionManager(470) - Participating in existing transaction
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.icatch.jta.Sync2Sync@7fdf7359 ) for transaction 192.168.1.207.tm169812958119800001
AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleADataSource': getConnection()...
AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleADataSource': init...
CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132393538313139383030303031:3139322E3136382E312E3230372E746D31 ) for transaction 192.168.1.207.tm169812958119800001
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@908670c5 ) for transaction 192.168.1.207.tm169812958119800001
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: calling prepareStatement(insert into single_a_jpa (test_text) values (?),1)...
JtaTransactionManager(470) - Participating in existing transaction
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.icatch.jta.Sync2Sync@24536f07 ) for transaction 192.168.1.207.tm169812958119800001
AbstractDataSourceBean(32) - AtomikosDataSoureBean 'jtaSingleBDataSource': getConnection()...
AbstractDataSourceBean(28) - AtomikosDataSoureBean 'jtaSingleBDataSource': init...
CompositeTransactionImp(32) - addParticipant ( XAResourceTransaction: 3139322E3136382E312E3230372E746D313639383132393538313139383030303031:3139322E3136382E312E3230372E746D32 ) for transaction 192.168.1.207.tm169812958119800001
CompositeTransactionImp(32) - registerSynchronization ( com.atomikos.jdbc.AtomikosConnectionProxy$JdbcRequeueSynchronization@908670c5 ) for transaction 192.168.1.207.tm169812958119800001
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: calling prepareStatement(insert into single_b_jpa (test_text) values (?),1)...
JtaTransactionManager(833) - Initiating transaction rollback
CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812958119800001
AbstractConnectionProxy(24) - Forcing close of pending statement: Pooled statement wrapping physical statement null
AbstractConnectionProxy(24) - Forcing close of pending statement: Pooled statement wrapping physical statement null
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: isClosed()...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: calling getWarnings...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: calling clearWarnings...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@32e5af53: close()...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: isClosed()...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: calling getWarnings...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: calling clearWarnings...
AtomikosConnectionProxy(32) - atomikos connection proxy for Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@22f80e36: close()...
CompositeTransactionImp(32) - rollback() done of transaction 192.168.1.207.tm169812958119800001
Spring boot 3.1.4
위 내용까지가 Spring boot 2.7.16 버전에 대한 예제였습니다. 그럼 이번에는 위에서 언급 한 대로 Spring boot 3.1.4에 대한 예제를 설명하도록 하겠습니다. 해당 예제에 대한 테스트 구성은 위 Spring boot 2.7.16과 같기에 자세한 설명은 생략하고 변경 점에 관해서만 설명하도록 하겠습니다.
build.gradle
2.7.16
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.16' // 변경 전
id 'io.spring.dependency-management' version '1.1.3'
}
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
runtimeOnly 'org.postgresql:postgresql'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.1'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos' // 변경 전
}
3.1.4
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.4' // 변경 후
id 'io.spring.dependency-management' version '1.1.3'
}
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
runtimeOnly 'org.postgresql:postgresql'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.1'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.atomikos:transactions-spring-boot3-starter:6.0.0' // 변경 후
}
2.7.16은 Spring에서 제공하는 spring-boot-starter-jta-atomikos
를 의존하였지만 3.1.4에선 Spring이 아닌 Atomikos에서 제공하는 transactions-spring-boot3-starter:6.0.0
을 의존해야 합니다. Atomikos에서 JakartaEE 관련 대응한 라이브러리 버전이 6.0.0인데 spring-boot-starter-jta-atomikos
최신 버전인 2.7.16에선 JakartaEE에 대응되지 않는 Atomikos 4.0.x를 의존하기 때문입니다. Spring의 spring-boot-starter-jta-atomikos
의존이 Atomikos 6.0.0을 의존하기 전까지는 Atomikos의 transactions-spring-boot3-starter:6.0.0
를 의존해야 할 듯합니다.
AtomikosDataSourceBean
2.7.16의 spring-boot-2.7.16.jar에는 Atomikos에 대해 org.springframework.boot.jta.atomikos
로 기본 내장되어 있었으나 3.1.4부터는 제외 되었습니다. 그래도 다행이라면 다행인게 위에서 언급한 Atomikos의 transactions-spring-boot3-starter:6.0.0
에서 기본으로 내장되어 있던 클래스들을 지원하고 있습니다. 그러나 문제가 될 수 있는 부분이 같은 클래스명이 다른 패키지에 존재하고 있기 때문에 import를 잘못하면 의도치 않게 동작할 수 있습니다.
AtomikosDataSourceBean 클래스가 대표적인 예인데 해당 클래스는 com.atomikos.jdbc
패키지에도 있고 com.atomikos.spring
패키지에도 있습니다. 만약 import 할 때 com.atomikos.spring
패키지가 아닌 com.atomikos.jdbc
패키지를 의존하게 된다면 application.yml(xml)에 설정한 Atomikos 설정 내용을 정상 적용할 수 없습니다.
즉 import를 할 때 패키지를 유심하게 살피셔야 합니다.
2.7.16
JtaSingleADatasource
, JtaSingleBDatasource
클래스
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
3.1.4
JtaSingleADatasource
, JtaSingleBDatasource
클래스
import com.atomikos.spring.AtomikosDataSourceBean;
JakartaEE로 인한 패키지명 변경
JavaEE에서 JakartaEE로 변경되면서 import 부분도 변경 되었습니다.
2.7.16
JtaSingleConfig
클래스
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
SingleAJpa
, SingleBJpa
클래스
import javax.persistence.*;
3.1.4
JtaSingleConfig
클래스
import jakarta.transaction.TransactionManager;
import jakarta.transaction.UserTransaction;
SingleAJpa
, SingleBJpa
클래스
import jakarta.persistence.*;
Hibernate-Dialect
해당 부분은 Postgresql에 한정입니다.
2.7.16에서 의존하는 hibernate-core-5.6.15.Final.jar의 Postgresql Dialect 클래스가 아래와 같이 있었으나
PostgreSQL9Dialect
PostgreSQL10Dialect
PostgreSQL81Dialect
PostgreSQL82Dialect
PostgreSQL91Dialect
PostgreSQL92Dialect
PostgreSQL93Dialect
PostgreSQL94Dialect
PostgreSQL95Dialect
3.1.4에서 의존하는 hibernate-core-6.2.9.Final.jar에서는 위 클래스들이 사라지고
PostgreSQLDialect
와 같이 변경되었기에 hibernate.dialect
부분도 변경되었습니다.
2.7.16
JtaJpaSingleAConfig
, PostgresqlSingleAJpaConfig
, JtaJpaSingleBConfig
, PostgresqlSingleBJtaJpaConfig
클래스
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQL95Dialect");
3.1.4
JtaJpaSingleAConfig
, PostgresqlSingleAJpaConfig
, JtaJpaSingleBConfig
, PostgresqlSingleBJtaJpaConfig
클래스
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
기타
기타 부분은 수정이 필요한 부분이 아니기에 자세한 설명은 생략하겠습니다.
Atomikos 로그 형식 변경
Atomikos 트랜잭션 로그 파일에 저장 되는 형식 변경
마치며
요즘은 MSA 아키텍처를 많이 사용하는 추세이므로 MSA 구조상 하나의 도메인에는 하나의 DB만 사용하는 식이라 이처럼 하나의 애플리케이션에서 여러 DB에 접속하는 상황은 지양할 것이라고 생각합니다.
그러나 MSA는 트랜잭션이 분산될 수밖에 없는 구조이기에 트랜잭션에 관련된 처리에 많이 고민해야 하는 부분도 존재하고 인프라 구축에 대한 난이도 및 비용에 대한 부분도 상당한 편입니다.
만약 MSA는 아니더라도 도메인 기반 설계를 하고 싶고 도메인마다 DB가 다른 경우나 CQRS 패턴과 같이 메시지 큐를 활용해서 Command와 Query DB 동기화가 하기 어려운 상황이라면 JTA를 도입하는 것도 좋은 방안이 되지 않을까 생각합니다. (물론 이 경우는 둘 다 RDB여야 합니다. 현재 Nosql에 대한 부분은 지원이 안되는 걸로 알고 있으며 된다 하더라도 확인은 못 해봤습니다.)
마지막으로 해당 예제 프로젝트의 링크는 버전 별로 2.7.16, 3.1.4에서 확인할 수 있습니다. 마지막으로 긴 글이라면 긴 글을 끝까지 읽어주셔서 감사드리고 하시는 개발에 항상 좋은 성과만 있기를 기도드리겠습니다.
감사합니다.