1. MongoDB에서 트랜잭션을 도입한 이유
교내 캡스톤 디자인 프로젝트인 FitTrip에서 저는 채팅 서비스를 개발했습니다.
채팅 서비스의 구조는 아래와 같습니다.
1번 유저가 특정 채팅 서비스로 채팅을 보내면 채팅 서비스에서는 해당 채팅을 MongoDB에 저장하고 카프카의 채팅 토픽으로 전송합니다. 그러면 해당 토픽을 구독하고 있는 다른 채팅 서비스들은 채팅 데이터를 가져와 1번 유저와 같은 채팅방에 있는 유저들에게 채팅을 전송합니다.
여기서 문제는 1번 유저가 MongoDB에 데이터 저장을 실패했는데 해당 데이터를 카프카에 보낸 경우입니다.
이럴 경우 같은 채팅방에 있는 유저들은 실시간으로 채팅을 볼 수 있겠지만 채팅방을 나갔다가 다시 들어오면 해당 채팅을 볼 수 없는 문제가 발생합니다.(채팅방을 들어왔을 때는 채팅 목록을 MongoDB에서 조회해 온다.)
그래서 데이터를 저장하는 행위와 카프카로 데이터를 보내는 행위를 하나의 트랜잭션으로 묶기 위해 트랜잭션의 ACID속성을 필요로 하게 되었습니다
MongoDB는 태생은 ACID보다 BASE 속성(Basically Available, 일관성)을 우선시하여 설계되었기 때문에 트랜잭션을 지원하지만 선택사항으로 남아있습니다. 그렇기 때문에 2가지 중 하나를 선택해야 하는 고민을 하게 되었습니다. RDB로 마이그레이션 해야 할까? 아니면 트랜잭션만 적용시켜서 nosql의 장점을 좀 더 유지해야 할까?
고민 끝에 트랜잭션을 적용하고 좀 더 nosql의 장점을 활용해 보자고 결정했습니다.
mongoDB의 트랜잭션 지원 참조
단일 도큐먼트 트랜잭션 지원 & 다중 도큐먼트 트랜잭션 지원
https://www.mongodb.com/docs/manual/core/transactions/
참고
트랜잭션(Transaction)이란?
트랜잭션은 데이터베이스 작업의 논리적인 단위로, 단일 작업들의 그룹을 의미합니다.
단일 작업은 쉽게 말해 더 이상 나눌 수 없는 최소한의 처리 단위입니다.
예를 들어 A 씨는 xx은행을 통해 친구 B 씨에게 10000원을 송금해야 할 일이 생겼다고 가정하겠습니다.
그래서 송금을 하던 도중, 10000원이 출금되었는데 xx은행에서 오류가 발생해 A 씨의 계좌에선 10000원이 빠져나갔지만, B 씨의 계좌에 10000원을 받지 못했습니다. 10000원은 A씨도 B씨도 아닌 어딘가로 사라지게 됐습니다.
앞 예제에서 A 씨의 계좌에서 10000원이 출금되는 현상을 하나의 단일 작업로 볼 수 있고, B 씨의 게좌에 입금이 되는 것을 또 하나의 단일 작업으로 볼 수 있어, 송금을 트랜잭션으로 묶을 수 있습니다. 실패를 방지하기 위해 트랜잭션을 사용합니다.
만약 정상적으로 트랜잭션이 완료되었다면, B 씨의 계좌에 10000원이 입금되면서 오류가 발생했을 것이고, 오류가 발생했으면 롤백(rollback: 트랜잭션 시작 이전의 상태로 돌리는 행위)이 되어 A 씨 계좌에서 10000원이 출금되지 않습니다.
반대로 오류가 없었다면 A 씨의 계좌에서 10000원이 정상적으로 빠져나간 뒤에는 B 씨의 계좌에 10000원이 입금되었을 것입니다. 정상적으로 데이터가 데이터베이스에 반영이 되었을 경우 이를 커밋(commit)이라 합니다.
트랜잭션의 특성(ACID)
ACID는 Atomicity(원자성), Consistency(일관성), Isolation(독립성), Durability(지속성)의 약자로, 트랜잭션의 특성을 말합니다. 이 네 개의 속성을 모두 만족시켜야 트랜잭션이라고 할 수 있습니다. 위 예시에서 정상적인 트랜잭션이 진행되었다고 가정하고 ACID가 뭔지 알아보겠습니다.
- Atomicity(원자성): Transacion 내의 단일 테스크들이 모두 성공을 하거나 모두 실패를 해야 한다.
- 송금 작업이 원자성의 특성을 가지고 있습니다. 출금과 입금은 하나의 트랜잭션으로 묶여있고, 출금이 되고 입금까지 다 되어야 성 공이 되고 하나의 작업이라도 실패하면 rollback 되어야 하기 때문입니다.
- Consistency (일관성): 일관성은 트랜잭션이 실행되기 전, 트랜잭션이 실행된 후의 상태가 일관되어야 한다.
- A 씨의 계좌 잔액 + B 씨의 계좌 잔액의 합이 트랜잭션이 발생하기 전과 트랜잭션이 발생한 후와 일치해야 일관성 특성을 만족합니다.
- Isolation(독립성): 여러 개의 트랜잭션이 동시에 실행될 때, 각각의 트랜잭션이 다른 트랜잭션에 영향을 주면 안 된다.
- 다른 제3자가 송금 작업을 수행할 경우에, A와 B의 계좌에선 영향을 주어서는 안 됩니다.
- Durability(지속성): 트랜잭션이 성공한 이후에, 그 결과가 영구적으로 저장되어야 한다.
- 송금 작업이 성공적으로 완료되었으면 A 씨의 계좌에선 10000원이 출금되고 B 씨의 계좌에서는 10000원이 영구적으로 저장되어야 합니다.
2. @Transactional
현재 채팅 서비스는 Spring Boot를 이용하고 있으니 스프링에서 지원하는 트랜잭션을 적용하는 방법부터 알아보겠습니다.
@Transactional 동작 원리
Transactional 어노테이션을 달면 Spring은 해당 메서드를 AOP를 사용해서 프록시 객체를 생성하여 메서드가 호출될 때마다 Spring이 트랜잭션 시작 및 종료를 제어할 수 있습니다. 트랜잭션은 추상화된 TransactionManager의 의해 관리됩니다.
@Transactional을 사용하지 않은 트랜잭션 적용 코드
public class businessService {
// 트랜잭션 매니저
private final PlatformTransactionManager transactionManager;
public void basicLogic(String userId) throws SQLException {
// 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 비즈니스 로직
bizLogic(userId);
transactionManager.commit(status); // 성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
}
private void bizLogic(String userId) throws SQLException {
...
}
}
@TranSactional을 적용한 코드
public class businessService {
@Transactional
public void basicLogic(String userId) throws SQLException {
bizLogic(userId);
}
private void bizLogic(String userId) throws SQLException {
...
}
}
보시는 바와 같이 @Transactional을 이용하면 Spring이 프록시 객체를 생성하여 set autocommit false;(수동 커밋 모드) 속성을 시작으로 성공하면 commit, 실패하면 rollback 과정을 수행할 수 있도록 도와줍니다. 트랜잭션 처리 로직을 분리하게 되는 거죠.
3. MongoDB에 트랜잭션을 적용하기 위해서 필요한 것들
트랜잭션은 추상화된 TransactionManager가 관리한다고 했습니다.
근데 Bean객체로 올린 적이 없는데 어떻게 된 걸까요?
Spring Boot는 현재 등록된 라이브러리를 보고 자동으로 스프링 컨테이너에 자동으로 올려줍니다.
PlatformTransactionManager
mongoTransactionManager
그러나 mongoDB는 트랜잭션은 선택이기 때문에 Spring Boot에서 자동으로 올려주지 않기 때문에 mongoDB의 트랜잭션을 관리하는 mongoTransactionManager를 직접 스프링 컨테이너에 올려주어야 합니다.
@Configuration
@EnableTransactionManagement
public class MongoConfig extends AbstractMongoClientConfiguration {
@Value("${spring.data.mongodb.uri}")
private String connectionString;
@Value("${spring.data.mongodb.database}")
private String databaseName;
@Bean
public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
@Bean
public MongoTemplate mongoTemplate() {
return new MongoTemplate(mongoClient(), databaseName);
}
@Override
public MongoClient mongoClient() {
ConnectionString connectionString = new ConnectionString(this.connectionString);
MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.build();
return MongoClients.create(mongoClientSettings);
}
@Override
protected String getDatabaseName() {
return databaseName;
}
}
@Configuration을 통해 설정클래스를 명시하고 스프링이 Bean객체를 등록할 수 있게 도와줍니다.
@EnableTransactionManagement은 @Transactional을 찾아서 트랜잭션 범위를 활성화하는 기능을 합니다.
@Value 어노테이션을 통해 yml파일에 지정된 값을 가져올 수 있도록 합니다.
MongoDatabaseFactory는 com.mongodb.client.MongoDatabase 인스턴스 가져오도록 도움을 줍니다.
(mongoDB 데이터베이스의 논리적 연결)
애플리케이션을 실행하면 아래와 같은 예외가 발생합니다.
com.mongodb.MongoCommandException: Command failed with error 263
(ShardingOperationFailed): 'Transaction numbers are only allowed on a replica set
member or mongos' on server localhost:27017.
The full response is { "ok" : 0.0, "errmsg" : "Transaction numbers are only
allowed on a replica set member or mongos", "code" : 263,
"codeName" : "ShardingOperationFailed" }
해당 예외는 레플리카 세트가 아닌 MongoDB 환경에서 트랜잭션을 관리하기 위해 MongoTransactionManager를 사용하는 경우 Spring Data MongoDB는 트랜잭션 시작 프로세스 중에 예외를 발생시킵니다.
replica set
예외를 통해서 mongoDB는 스프링부트에서 @Transactional을 사용하기 위해 replicaSet 환경을 구축해야 한다는 것을 알았습니다. MongoDB 트랜잭션은 데이터의 일관성과 무결성을 유지하면서 원자적으로 실행해야 하는 여러 작업을 트랜잭션에 포함하는 경우가 많기 때문에 레플리카 세트 내에서 작동하도록 설계되었다고 하네요.
https://www.mongodb.com/docs/manual/replication/
그래서 필요한 것을 정리하자면
1. 스프링 컨테이너에 mongoDB 트랜잭션을 관리하는 mongoTransactionManager 올리고
2. Transaction을 사용하기 위해 mongoDB를 레플리카 셋 환경으로 구축
3. @Transaction이 잘 동작하는지 확인
(트랜잭션 사용하려고 하는데 replica set까지 구성하라니...🙃)
4. MongoDB에서 레플리카 셋(replica set)과 샤드 클러스터(shard cluster)
레플리카 셋을 직역하자면 '복제 집합'입니다.
MongoDB에서 레플리카 셋은 동일한 데이터를 관리하는 인스턴스 그룹이라고 생각하면 됩니다.
레플리카 셋은 중복성을 제공하고 가용성을 높입니다.
레플리카 셋은 하나의 primary 서버와 하나 이상의 secondary 서버로 구성됩니다.
Primary 서버에서는 모든 Write operation을 담당합니다.
즉 데이터가 생성되고, 업데이트되고 삭제되는 부분을 담당합니다.
Secondary 서버에서는 데이터 복사본을 유지 관리 담당합니다.
Primary 서버로부터 동일한 데이터를 저장하고 변경된 사항을 비동기로 복제합니다.
Transacion 관점에서 봤을 경우, 롤백이 일어날 때, Primary 서버와 Secondary 서버 간의 데이터 복제를 통해 롤백이 되어 데이터 일관성을 유지합니다.
샤딩은 데이터를 여러 시스템에 분산시키는 방법입니다.
MongoDB에서 샤딩은 데이터 세트 또는 처리량이 많은 애플리케이션이 있을 경우 사용합니다.
샤드 클러스터는 하나 이상의 mongos(router역할), 두 개 이상의 샤드, 그리고 Config server로 샤드 클러스터를 구성합니다.
- Shard: 샤드는 샤딩된 데이터로 구성되어 있다. 각 샤드는 레플리카 셋으로 배포할 수 있다.
- Mongos: mongos는 router 역할을 하고 클라이언트의 요청을 받는 샤드 클러스터의 인터페이스 역할을 한다.
- Config server: 구성 서버에는 메타 데이터와 클러스터에 대한 설정을 담고 있다.
결정적인 요소는 데이터의 크기, 처리량, 성능 요구 사항, 확장 가능성 등과 같은 요구 사항을 고려하여 선택해야 합니다. 일반적으로 작은 규모의 애플리케이션에서는 레플리카 셋을 사용하고, 대규모 및 고성능 환경에서는 샤드 클러스터를 사용하는 경향이 있습니다.
레플리카 셋은 어떻게 이루어 질까?
레플리카 셋에서의 node들은 위한 투표권을 가지게 되는데, 만약 primary에서 서버 장애가 생겼을 시 투표권으로 secondary
멤버 중 한 노드가 primary가 됩니다. 레플리카 셋은 최대 50개까지 노드를 가질 수 있으나 투표권을 가질 수 있는 노드는 7개까지만 구성이 가능합니다. 선거를 통해 primary로 선정된 노드보다 최신 데이터를 가지고 있는 secondary가 존재할 시, 최신 데이터를 가진 secondary node는 선정된 primary 데이터를 롤백합니다. 그래서 MongoDB를 사용할 때, 롤백을 해서 데이터가 손실되는 경우가 발생할 수 있습니다. 롤백을 해서 데이터가 손실되는 경우엔 WriteConcern옵션을 조정해서 Rollback을 방지해야 합니다.
왜 MongoDB에서는 레플리카 셋이 필요할까?
트랜잭션은 논리적 세션의 개념으로 만들어졌기 때문에, 레플리카 셋 환경에서만 가능한 oplog와 같은 기술이 필요하다고 Mongodb 직원이 대답해 줬습니다. (oplog: Operation Log의 약자로, 레플리카 셋의 데이터 동기화를 위해서 내부에서 발생하는 로그를 기록한 것)
위에서 트랜잭션 예시와 MongoDB에서의 트랜잭션을 위한 레플리카 셋에 대해서 설명했습니다.
이제 간단한 트랜잭션 테스트를 해보겠습니다.
Docker에서 MongoDB 레플리카 셋 환경을 구축한 뒤, 자바에서 테스트를 진행할 예정입니다.
5. Docker로 레플리카 셋 (Replica Set) 환경 구성하기
version: "3.1"
services:
mongodb1:
image: mongo
container_name: mongo1
hostname: mongo1
restart: always
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: root
command: mongod --replSet rs0 --keyFile /etc/mongodb.key --bind_ip_all
volumes:
- ./db1:/data/db
- ~/.ssh/mongodb.key:/etc/mongodb.key
mongodb2:
image: mongo
container_name: mongo2
hostname: mongo2
restart: always
ports:
- "27018:27018"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: root
command: mongod --replSet rs0 --keyFile /etc/mongodb.key --bind_ip_all
volumes:
- ./db2:/data/db
- ~/.ssh/mongodb.key:/etc/mongodb.key
replica set 인증에 사용될 key file 생성
# mongodb 키 생성
sudo openssl rand -base64 756 > ~/.ssh/mongodb.key
# 권한 설정
sudo chmod 400 ~/.ssh/mongodb.key
# 제대로 key가 생성되었는지 확인
cat ~/.ssh/mongodb.key
docker compose 백그라운드로 실행
docker-compose up -d
docker container가 정상적으로 올라왔는지 확인
docker ps -a
docker container안의 mongo를 들어가 보겠습니다.
# docker container 접속
docker exec -it mongo1 /bin/bash
# root 계정 몽고 쉘 접속
mongosh -u root -p root
레플리카 셋 초기화
- use admin
- rs.initiate() (초기화)
- direct가 시간이 지나면 direct: primary로 변환됩니다.
# admin 데이터베이스 사용
use admin
# replication 초기화
rs.initiate()
# mongo2 복제세트 추가
rs.add({_id: 1, host: "mongo2:27017"})
rs.initiate()
rs.add()
레플리카 셋 설정 확인
# 레플리카 셋 설정 정보 확인
rs.config()
# 레플리카 셋 상태정보 확인
rs.status()
rs.config()
rs.status()
mongo Compass로 Local DB 접속
엔드포인트
mongodb://root:root@127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.2.10
접속 완료 확인
그럼 이제 아래 테스트 코드를 실행해서 DB에 잘 저장되는지 mongo Compass로 확인해 보겠습니다.
@Test
void save_data() {
TestData testData = new TestData("error", 10);
Long saveTestDataId = testService.saveData(testData);
// 데이터가 저장되었는지 검증
Optional<MongoData> findData = testRepository.findById(saveTestDataId);
assertTrue(findData.isPresent());
assertEquals(saveTestDataId, findData.get().getDataId());
}
테스트 코드를 실행하면 아래와 같은 예외가 발생했습니다.
Command failed with error 10107 (NotWritablePrimary): 'not primary' on server 127.0.0.1:27017. The full response is {"errorLabels": ["RetryableWriteError"], "topologyVersion": {"processId": {"$oid": "670a3dab8e1520fa9d34d7d4"}, "counter": 10}, "ok": 0.0, "errmsg": "not primary", "code": 10107, "codeName": "NotWritablePrimary", "$clusterTime": {"clusterTime": {"$timestamp": {"t": 1728726639, "i": 1}}, "signature": {"hash": {"$binary": {"base64": "AaxZEZTjOvpUw88EANp7FLrpPhg=", "subType": "00"}}, "keyId": 7424815199883886598}}, "operationTime": {"$timestamp": {"t": 1728726639, "i": 1}}}
org.springframework.data.mongodb.UncategorizedMongoDbException: Command failed with error 10107 (NotWritablePrimary): 'not primary' on server 127.0.0.1:27017. The full response is {"errorLabels": ["RetryableWriteError"], "topologyVersion": {"processId": {"$oid": "670a3dab8e1520fa9d34d7d4"}, "counter": 10}, "ok": 0.0, "errmsg": "not primary", "code": 10107, "codeName": "NotWritablePrimary", "$clusterTime": {"clusterTime": {"$timestamp": {"t": 1728726639, "i": 1}}, "signature": {"hash": {"$binary": {"base64": "AaxZEZTjOvpUw88EANp7FLrpPhg=", "subType": "00"}}, "keyId": 7424815199883886598}}, "operationTime": {"$timestamp": {"t": 1728726639, "i": 1}}}
이유는 yml에서 연결한 mongo1이 primary가 아니기 때문이라고 합니다.
그래서 몽고 컨테이너에 접속해서 rs.config()를 통해 priority를 체크해 보겠습니다.
rs0 [direct: primary] admin> rs.config()
{
_id: 'rs0',
version: 3,
term: 1,
members: [
{
_id: 0,
host: 'mongo1:27017',
arbiterOnly: false,
buildIndexes: true,
hidden: false,
priority: 1,
tags: {},
secondaryDelaySecs: Long('0'),
votes: 1
},
{
_id: 1,
host: 'mongo2:27017',
arbiterOnly: false,
buildIndexes: true,
hidden: false,
priority: 1,
tags: {},
secondaryDelaySecs: Long('0'),
votes: 1
}
],
protocolVersion: Long('1'),
writeConcernMajorityJournalDefault: true,
settings: {
chainingAllowed: true,
heartbeatIntervalMillis: 2000,
heartbeatTimeoutSecs: 10,
electionTimeoutMillis: 10000,
catchUpTimeoutMillis: -1,
catchUpTakeoverDelayMillis: 30000,
getLastErrorModes: {},
getLastErrorDefaults: { w: 1, wtimeout: 0 },
replicaSetId: ObjectId('670a3e168e1520fa9d34d819')
}
}
priority를 보면 둘 다 1로 되어 있어 전부 같은 우선순위를 가지고 있는 걸 볼 수 있습니다.
rs.status()를 통해 현재 mongo1과 mongo2의 상태를 확인해 보겠습니다.
members: [
{
_id: 0,
name: 'mongo1:27017',
health: 1,
state: 2,
stateStr: 'SECONDARY',
uptime: 4784,
optime: { ts: Timestamp({ t: 1728729176, i: 1 }), t: Long('2') },
optimeDate: ISODate('2024-10-12T10:32:56.000Z'),
lastAppliedWallTime: ISODate('2024-10-12T10:32:56.352Z'),
lastDurableWallTime: ISODate('2024-10-12T10:32:56.352Z'),
syncSourceHost: 'mongo2:27017',
syncSourceId: 1,
infoMessage: '',
configVersion: 3,
configTerm: 2,
self: true,
lastHeartbeatMessage: ''
},
{
_id: 1,
name: 'mongo2:27017',
health: 1,
state: 1,
stateStr: 'PRIMARY',
uptime: 3850,
optime: { ts: Timestamp({ t: 1728729176, i: 1 }), t: Long('2') },
optimeDurable: { ts: Timestamp({ t: 1728729176, i: 1 }), t: Long('2') },
optimeDate: ISODate('2024-10-12T10:32:56.000Z'),
optimeDurableDate: ISODate('2024-10-12T10:32:56.000Z'),
lastAppliedWallTime: ISODate('2024-10-12T10:32:56.352Z'),
lastDurableWallTime: ISODate('2024-10-12T10:32:56.352Z'),
lastHeartbeat: ISODate('2024-10-12T10:32:57.673Z'),
lastHeartbeatRecv: ISODate('2024-10-12T10:32:57.689Z'),
pingMs: Long('0'),
lastHeartbeatMessage: '',
syncSourceHost: '',
syncSourceId: -1,
infoMessage: '',
electionTime: Timestamp({ t: 1728725328, i: 1 }),
electionDate: ISODate('2024-10-12T09:28:48.000Z'),
configVersion: 3,
configTerm: 2
}
],
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1728729176, i: 1 }),
signature: {
hash: Binary.createFromBase64('VFohQzlJLzGpo9ZXOV7Slevk4Xk=', 0),
keyId: Long('7424815199883886598')
}
},
operationTime: Timestamp({ t: 1728729176, i: 1 })
}
mongo1이 secondary로 되어있고, mongo2가 primary로 되어있습니다.
아마 자동선출 과정에서 우선순위가 같다 보니 mongo1이 아닌 mongo2가 primary가 된 거 같습니다.
그래서 primary인 mongo2 컨테이너에 접속해서
myReplicaSet [direct: primary] test> var cfg = rs.conf()
myReplicaSet [direct: primary] test> cfg.members[0].priority=2
myReplicaSet [direct: primary] test> rs.reconfig(cfg)
를 입력해 주면 mongo1이 primary로 바뀌게 됩니다.
rs.config(), rs.status()를 입력하여 우선순위와 상태를 확인해 보겠습니다.
확인해 보면 우선순위와 상태가 잘 바뀐 걸 확인할 수 있습니다.
이제 다시 테스트 코드를 실행하면 아래와 같이 정상 실행이 됩니다.
DB에도 마찬가지로 데이터가 잘 저장이 됐습니다.
6. 트랜잭션 적용되었는지 테스트
그럼 이제 최종 목적인 트랜잭션이 잘 적용되는지 확인해 보겠습니다.
MongoData
@Getter
@Document(collection = "mongoData")
public class MongoData {
@Transient
public static final String SEQUENCE_NAME = "data_sequence";
@Id
private Long dataId;
@Field
private String value;
@Field
private int number;
public MongoData(String value, int number) {
this.value = value;
this.number = number;
}
public void generateSequence(Long dataId) {
this.dataId = dataId;
}
}
TestService
saveData 메서드를 호출하면 mongoData를 저장하고 입력 파라미터 testData에 value값이 error라면 예외를 발생
@Slf4j
@Service
@RequiredArgsConstructor
public class TestService {
private final TestRepository testRepository;
private final SequenceGenerator sequenceGenerator;
@Transactional
public Long saveData(TestData testData) {
MongoData mongoData = TestData.createMongoData(testData);
mongoData.generateSequence(sequenceGenerator.generateSequence(MongoData.SEQUENCE_NAME));
MongoData save = testRepository.save(mongoData);
if (testData.getValue().equals("error")) {
throw new RuntimeException("예외 발생");
}
return save.getDataId();
}
}
Test code
@Transactional 어노테이션이 붙어있는 saveData에서 예외를 발생시키면 mongoData가 저장되지 않고 rollback이 수행되는지 확인
@Slf4j
@SpringBootTest
class TestServiceTest2 {
@Autowired
TestRepository testRepository;
@Autowired
TestService testService;
@Test
void save_data() {
long initialCount = testRepository.count();
try {
TestData testData = new TestData("error", 10);
testService.saveData(testData);
} catch (Exception exception) {
log.info("transaction rollback");
}
long rollbackCount = testRepository.count();
log.info("rollbackCount : " + rollbackCount);
assertThat(initialCount).isEqualTo(rollbackCount);
}
}
테스트 코드를 통해 정상적으로 transaction rollback이 동작하는 것을 확인할 수 있습니다.
(rollbackCount가 1인 이유는 트랜잭션 실험하기 전 DB에 데이터를 한 개 저장을 해서 1이 나왔습니다.)
실제 DB에도 저장이 안 됐는지 확인해 보겠습니다.
기존에 저장한 데이터만 있는 걸 확인할 수 있습니다.
참고
https://www.mongodb.com/docs/manual/replication/
https://www.mongodb.com/docs/manual/core/transactions/
https://www.mongodb.com/ko-kr/docs/manual/sharding/
https://www.baeldung.com/spring-data-mongodb-transactions - eaeldung 트랜잭션 적용
https://ozofweird.tistory.com/entry/MongoDB-%EC%9E%A0%EA%B8%88-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98
'프로젝트 > FitTrip' 카테고리의 다른 글
트러블 슈팅 - IN 연산자를 활용하여 채팅 목록 조회 92% 성능 최적화 (0) | 2024.07.02 |
---|---|
트러블 슈팅 - 웹소켓 연결 요청 JWT 검증 문제 (0) | 2024.06.30 |
개발 기록 - 채팅 서비스 몽고DB 데이터 모델링 (0) | 2024.06.28 |
트러블 슈팅 - 채팅 서비스 scalue out 문제 (0) | 2024.06.27 |
개발 기록 - 개발 언어 및 기반 기술 조사 (0) | 2024.06.27 |