MongoDB 4.0 이전 버전에선 Single-Document Transactions만을 지원하였습니다.
RDB는 데이터들을 정규화해서 서로 다른 테이블에 저장하는 반면 MongoDB는 정규화를 거치지 않고 하나의 Document에 몽땅 저장 하기 때문에 MongoDB의 지향점을 생각하면 Single-Document Transactions이면 충분했겠지만 세상은 우리의 뜻(?)대로만 살 수 없기에 4.0 버전부터 Multi-Document Transactions이 지원되게 되었습니다. 여담이지만 4.0 이전 버전에서 Multi-Document Transactions을 위해선 2-Phase-Commits과 같은 방법을 개발자가 직접 처리해야 했습니다.
그럼 버전별로 달라진 점을 간략 설명하자면
v4.0
- Multi-Document Transactions 지원 시작. 단 replica sets 구성이여야 함
- MongoDB driver 4.0 이상 버전 사용
v4.2
- Multi-Document Transactions 지원 범위가 sharded clusters까지 확장
- MongoDB driver 4.2 이상 버전 사용
v4.4
- Transactions에서 컬렉션과 인덱스 생성 가능. 단 샤드 간 쓰기 Transactions이 아니여야 함
- MongoDB driver 4.4 이상 버전 사용
그럼 Multi-Document Transactions을 지원하는 MongoDB를 설치하고 Spring Boot에서 정상적으로 동작하는지 확인 해보도록 하겠습니다.
MongoDB
- docker-compose 활용
- Multi-Document Transaction을 위한 MongoDB 3대 replica sets 구성
- 1대 Master, 2대 Sencondary
- Transactions에서 컬렉션 생성을 위해 4.4 버전 사용
예제 docker-compose은 해당 게시물 예제 Git 프로젝트에 docker-compose 폴더 안에
- mongo-4.2 MongoDB 4.2.5 버전
- mongo-4.4 MongoDB 4.4 버전
버전별 테스트를 위해 위 폴더별로 docker-compose yml 파일을 생성해두었습니다. 폴더 구조는 아래를 참고하시면 되겠습니다.
+--- mongo-4.2
| +--- docker-entrypoint-initdb.d
| | +--- rs-init.sh
| +--- mongo4_2-docker-compose.yml
+--- mongo-4.4
| +--- docker-entrypoint-initdb.d
| | +--- rs-init.sh
| +--- mongo4_4-docker-compose.yml
docker-compose.yml
version: '3.8'
services:
mongodb-4.4-1:
image: mongo:4.4
container_name: mongodb-4.4-1
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- 27017:27017
volumes:
- ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD", "/docker-entrypoint-initdb.d/rs-init.sh"]
entrypoint: ["/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs"]
depends_on:
- mongodb-4.4-2
- mongodb-4.4-3
links:
- mongodb-4.4-2
- mongodb-4.4-3
mongodb-4.4-2:
image: mongo:4.4
container_name: mongodb-4.4-2
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- 27018:27017
entrypoint: ["/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs"]
mongodb-4.4-3:
image: mongo:4.4
container_name: mongodb-4.4-3
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- 27019:27017
entrypoint: ["/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs"]
rs-init.sh
#!/bin/bash
mongo <<EOF
var config = {
"_id": "rs",
"version": 1,
"members": [
{
"_id": 1,
"host": "host.docker.internal:27017",
"priority": 3
}
, {
"_id": 2,
"host": "host.docker.internal:27018",
"priority": 2
}
, {
"_id": 3,
"host": "host.docker.internal:27019",
"priority": 1
}
]
};
rs.initiate(config, { force: true });
rs.status();
EOF
mongo <<EOF
use admin;
db.createUser( { user: "user", pwd: "password", roles: ["root"] });
EOF
Master(mongodb-4.4-1)에서 추가적으로 Shell을 실행하여 replica set 설정 및 관리자 계정을 생성합니다. 정상적으로 replica sets 구성이 되려면 몇 초 정도의 시간이 소요 됩니다.
Spring boot
- 2.7.1 버전
- spring-boot-starter-data-mongodb 사용
- 2.7.1 버전 사용 시 mongodb driver 버전은 4.6.1 사용
application.yml
mongodb:
master:
host: localhost
port: 27017
database: board
auth-database: admin
username: user
password: password
sencondary:
- host: localhost
port: 27018
- host: localhost
port: 27019
replica set 접속을 위한 접속 정보
Config
MongoDB 접속을 위해 Java Config을 사용하였습니다.
@Configuration
public class MongoDBConfig {
private final MongoDBProperties mongoDBProperties;
public MongoDBConfig(MongoDBProperties mongoDBProperties) {
this.mongoDBProperties = mongoDBProperties;
}
@Bean
public MongoClient mongoClient() {
Master master = mongoDBProperties.getMaster();
MongoCredential mongoCredential = MongoCredential.createCredential(master.getUsername()
, master.getAuthDatabase()
, master.getPassword().toCharArray());
List<ServerAddress> serverAddresses = new ArrayList<>();
serverAddresses.add(new ServerAddress(master.getHost(), master.getPort()));
List<Sencondary> sencondarys = mongoDBProperties.getSencondary();
for(Sencondary sencondary : sencondarys) {
serverAddresses.add(new ServerAddress(sencondary.getHost(), sencondary.getPort()));
}
MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
.credential(mongoCredential)
.applyToClusterSettings(b -> b.hosts(serverAddresses))
.build();
return MongoClients.create(mongoClientSettings);
}
@Bean
public MongoDatabaseFactorySupport<MongoClient> mongoDatabaseFactorySupport(MongoClient mongoClient) {
return new SimpleMongoClientDatabaseFactory(mongoClient, mongoDBProperties.getMaster().getDatabase());
}
@Bean
public MongoTransactionManager mongoTransactionManager(MongoDatabaseFactorySupport<MongoClient> mongoDatabaseFactorySupport) {
return new MongoTransactionManager(mongoDatabaseFactorySupport);
}
@Bean
public MongoTemplate mongoTemplate(MongoDatabaseFactorySupport<MongoClient> mongoDatabaseFactorySupport) {
MongoTemplate mongoTemplate = new MongoTemplate(mongoDatabaseFactorySupport);
((MappingMongoConverter) mongoTemplate.getConverter()).setTypeMapper(new DefaultMongoTypeMapper(null));
return mongoTemplate;
}
}
Entity
Multi-Document Transaction 테스트를 위한 목적으로 게시물 엔티티를 생성하였습니다.
/**
* <pre>
* MongoDB Multi-Document Transaction 테스트를 위해 생성한 게시물 엔티티
* </pre>
*/
@Document(collation = "posts")
public class Posts {
private final String memberId;
private final String text;
private final LocalDateTime registrationDate;
public Posts(String memberId, String text) {
this.memberId = memberId;
this.text = text;
this.registrationDate = LocalDateTime.now();
}
public String getMemberId() {
return memberId;
}
public String getText() {
return text;
}
public LocalDateTime getRegistrationDate() {
return registrationDate;
}
}
Service
Transaction 테스트를 위해 작성된 간단한 테스트 Service 클래스 입니다.
/**
* <pre>
* MongoDB Multi-Document Transaction 테스트를 위한 구현체
* </pre>
*/
@Service
public class PostsServiceImpl {
private final MongoTemplate mongoTemplate;
public PostsServiceImpl(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
/**
* <pre>
* 테스트 목적을 위한 게시물 저장 메서드
* </pre>
* @param posts
* @param isTransactionsTest 예외 발생 시 롤백이 수행되는지 확인 하기 위한 변수
*/
@Transactional(rollbackFor = Exception.class)
public void savePosts(Posts posts, boolean isTransactionsTest) {
this.mongoTemplate.insert(Objects.requireNonNull(posts), "posts");
this.mongoTemplate.insert(Objects.requireNonNull(posts), "posts_transactions");
if(isTransactionsTest) {
throw new IllegalStateException("트랜잭션 테스트를 위한 임시 예외 발생");
}
}
}
두 번째 매개변수인 isTransactionsTest
를 보시면 해당 변수의 값이 true인 경우 임의의 예외를 발생 시킵니다. 예외가 발생되면 성공적으로 Rollback이 수행되어야 합니다.
Test
정상 Insert 케이스
먼저 정상적으로 Insert가 되는지 확인해보겠습니다. 테스트 코드는 아래를 참고하시면 되겠습니다.
@Test
void mongodb_게시물_정상_케이스() {
Posts posts = new Posts("success_id", "mongodb Insert!");
this.postsServiceImpl.savePosts(posts, false);
}
테스트 메서드를 실행 시키면 데이터가 정상적으로 Insert 된 것을 확인 할 수 있습니다.
rs:PRIMARY> db.posts.find()
{ "_id" : ObjectId("62da3b0b11f3b1463dfc85d6"), "memberId" : "success_id", "text" : "mongodb Insert!", "registrationDate" : ISODate("2022-07-22T05:52:11.629Z") }
rs:PRIMARY> db.posts_transactions.find()
{ "_id" : ObjectId("62da3b0b11f3b1463dfc85d7"), "memberId" : "success_id", "text" : "mongodb Insert!", "registrationDate" : ISODate("2022-07-22T05:52:11.629Z") }
Rollback 케이스
그럼 이번엔 강제로 예외를 발생시키면 Rollback이 되는지 확인해보겠습니다. 테스트 코드는 아래를 참고하시면 되겠습니다.
@Test
void mongodb_게시물_임의_예외_발생_롤백_케이스() {
Posts posts = new Posts("fail_id", "mongodb Rollback!");
this.postsServiceImpl.savePosts(posts, true);
}
만약 Rollback이 수행 되지 않았으면 member_id 필드에 fail_id라는 값이 존재해야 합니다.
rs:PRIMARY> db.posts.find()
{ "_id" : ObjectId("62da3b0b11f3b1463dfc85d6"), "memberId" : "success_id", "text" : "mongodb Insert!", "registrationDate" : ISODate("2022-07-22T05:52:11.629Z") }
rs:PRIMARY> db.posts_transactions.find()
{ "_id" : ObjectId("62da3b0b11f3b1463dfc85d7"), "memberId" : "success_id", "text" : "mongodb Insert!", "registrationDate" : ISODate("2022-07-22T05:52:11.629Z") }
조회를 해보면 fail_id 값이 없는걸로 봐서 정상적으로 Rollback이 수행 되었음 알 수 있습니다.
그 외
MongoDB v4.2에서 테스트를 수행하려고 하면 아래와 같은 에러가 발생합니다.
Write operation error on server host.docker.internal:27017. Write error: WriteError{code=263, message='Cannot create namespace board.posts in multi-document transaction.', details={}}.; nested exception is com.mongodb.MongoWriteException: Write operation error on server host.docker.internal:27017. Write error: WriteError{code=263, message='Cannot create namespace board.posts in multi-document transaction.', details={}}.
Transactions에서 컬렉션 생성이 v4.4에서 부터 지원하기 때문에 v4.4 이하 버전인 v4.2에서는 위와 같은 에러가 발생합니다. 그러므로 v4.2에서는 사용하려는 컬렉션을 필히 생성해두어야 합니다.
마치며
본 작성글의 예제 Git은 GitHub - sungwookkim/mongodb-multi-transactions에서 확인하실 수 있습니다.