티스토리 뷰
트랜잭션
트랜잭션은 읽기나 쓰기 작업이 가능한 데이터베이스 작업을 하나 이상 포함하는 데이터베이스의 논리적 작업 단위를 의미한다. 데이터베이스 운영 시 여러 도큐먼트에 관한 읽기과 쓰기 작업이 필요한 경우가 많고, 이러한 도큐먼트들은 하나의 컬렉션에 있을 수도 있고, 여러 컬렉션에 분산되어 있을 수 있다.
몽고DB는 비관계형 데이터베이스이므로 전통적인 관계형 데이터베이스의 트랜잭션 방식을 그대로 적용할 수 없다. 몽고DB CRUD 블로그에서 설명했듯이 updateOne, findOneAndDelete, findOneAndReplace, findOneAndUpdate 등 Single Document에 대해서는 원자성을 보장한다. 몽고DB의 경우 insertMany처럼 전체 쿼리에 대해 일부 실패하면 그 시점까지만 반영되고 나머지는 반영되지 않는 것이 기본동작이다.
몽고DB는 2018년 버전 4.0의 출시와 함꼐 다중 문서 ACID 트랜잭션 지원을 시작했다. Replica Set의 경우 4.0부터, Sharding의 경우 4.2부터 Multi Document Transaction을 지원하고 있어 몽고DB도 전체 트랜잭션이 가능하다.
ACID 정의
ACID는 데이터베이스 트랜잭션의 핵심 특성을 나타내는 것으로, 원자성, 일관성, 격리성, 지속성을 의미한다.
- 원자성(Atomicity)
- 트랜잭션의 완전성을 보장하는 특성으로 트랜잭션에 포함된 모든 작업은 전부 성공하거나 전부 실패한다.
- 일관성(Consistency)
- 데이터 무결성을 보장하는 특성으로 트랜잭션이 완료된 후에도 모든 데이터는 정의된 규칙과 제약을 준수하는 일관된 상태를 유지해야 한다.
- 고립성(Isolation)
- 동시에 실행되는 트랜잭션 간의 독립성을 보장한다. 각 트랜잭션은 다른 트랜잭션의 중간 상태를 볼 수 없으며, 마치 자신만이 실행되고 있는 것처럼 작동해야 한다.
- 몽고DB의 WiredTiger 스토리지 엔진은 스냅샷 격리(Snapshot Isolation)를 기본적으로 제공하여 하나의 트랜잭션에서 쿼리는 항상 같은 결과를 반환하고, readConcern과 writeConcern 조합으로 조절할 수 있다.
- 영속성(Durability)
- 트랜잭션이 성공적으로 완료된 후의 데이터 보존을 보장한다. 한 번 커밋된 트랜잭션의 결과는 시스템 장애가 발생하더라도 영구적으로 보존되어야 한다.
- WiredTiger 스토리지 엔진을 통해 WAL(Write-ahead Log) 메커니즘을 구현하여 100밀리초 간격으로 트랜잭션을 디스크 저널에 자동으로 커밋함으로써 데이터의 영구성을 보장한다.
일관성
데이터베이스의 상태를 관리하는 일관성에 대해 자세히 알아보자. 데이터베이스의 일관성 모델은 크게 두 가지로 나눌 수 있고, 몽고DB는 이 두 가지 모델의 중간 특성에 있다.
먼저, 최종 일관성(eventual consistency)은 분산 데이터 시스템에서 가장 널리 사용되는 모델이다. 이 모델에서는 데이터가 업데이트되면 시간이 지남에 따라 모든 후속 읽기 작업에서 최신 커밋된 값을 확인할 수 있다. 분산 시스템에서 데이터는 서버 네트워크를 통해 복제되므로, 성능 측면에서 최종 일관성 모델이 가장 실용적인 선택이 된다.
다음, 강한 일관성(strong consistency) 모델은 모든 후속 읽기 작업에서 항상 최근에 커밋된 쓰기 값을 확인할 수 있도록 보장한다. 이를 위해서는 다음 읽기 작업이 시작되기 전에 모든 업데이트가 전체 서버에 전파되고 커밋되어야 한다. 그러나 이러한 특성은 분산 시스템에서는 성능 저하가 발생할 수 있다.
몽고DB의 ACID 구현
몽고DB의 데이터 모델링 방식은 전통적인 관계형 데이터베이스와는 차이가 있다. 관계형 데이터베이스가 데이터를 여러 테이블로 분리하는 반면, 몽고DB는 문서 내에 하위 문서와 배열을 활용하여 관련 데이터를 계층적으로 구조화한다. 몽고DB의 공식 백서에 따르면 대다수의 애플리케이션(약 80~90%)은 멀티 도큐먼트 트랜잭션이 필요하지 않다.
멀티 도큐먼트 ACID 트랜잭션이 필요한 이유 중 하나로 데이터의 규모가 몽고DB의 단일 문서 크기 제한인 16MB를 초과하는 경우가 있다. 데이터를 하나의 주 도큐먼트 내 하위 도큐먼트 및 배열로 저장하는 것이 불가능하여, 여러 문서에 걸친 트랜잭션 처리가 필수적이다.
Replica Set와 Sharding 클러스터에서의 몽고DB 트랜잭션 구성은 세심한 주의가 필요하다. 모든 트랜잭션을 시작할 때는 반드시 읽기 및 쓰기 보장 수준을 적절히 설정해야 하고, 몽고DB 4.4부터는 클러스터 전체에 걸쳐 읽기와 쓰기 보장 수준을 통합적으로 구성할 수 있다.
Write Concern
| Write Concern | 설명 | 특징 | ||
| ACKNOWLEDGED | 프라이머리 노드가 쓰기 작업을 메모리에 적용 후 바로 응답 | 빠른 응답 속도를 제공하지만, 장애 시 데이터 일관성이 낮을 수 있다. | ||
| UNACKNOWLEDGED | 요청을 보내면, 저장 유무 상관없이 성공을 응답 | 매우 빠른 응답을 제공하지만 데이터 손실 가능성이 있다. | ||
| MAJORITY | 세컨더리 노드까지 과반수의 합의가 된다면 응답 | 높은 데이터 일관성을 보장하며, 과반수에서 기록이 완료되므로 성능이 저하될 수도 있다. | ||
| W1, W2 | 세컨더리 노드에서 성공을 합의하는 갯수가 충족된다면 응답 | 특정 개수의 노드에 기록되었는지 제어 가능하며, 안정성과 성능 사이를 조절할 수 있다. | ||
| JOURNALED | 쓰기 작업이 디스크 저널에 기록이 된다면 응답 | 장애 시 복구가 가능하며 안전한 기록을 보장하지만, 성능 저하가 있을 수 있다. | ||
wrtieConcern은 몽고DB에서 레플리카 셋이나 샤딩된 클러스터에 대한 쓰기(write) 작업에 대해 확인 수준을 말한다. 몽고DB 5.0 이상의 버전에서는 클라이언트 수준의 majority 쓰기 보장을 기본값으로 사용한다.

위 그림은 writeConcern: { w: "majority" } 설정일 때, 몽고DB에 쓰기 요청을 할 때 응답이 오기까지의 과정을 나타낸다. 프라이머리 노드가 데이터를 먼저 메모리에 적용(Apply)한 뒤, 세컨더리 노드 과반수에 복제가 완료되면 클라이언트(Driver)에게 성공 응답을 보낸다.
Read Concern
| Read Concern | 설명 | 특징 | ||
| local | 현재 연결된 노드(Primary 또는 Secondary)의 메모리에 반영된 최신 데이터 읽기 | 롤백된 가능성이 있는 데이터를 읽어 데이터 일관성이 낮다. | ||
| majority | 레플리카 셋의 과반수 노드에 커밋된 데이터 읽기 | local 수준보다 안정적이며, 데이터가 장애 시에도 롤백되지 않는다. | ||
| snapshot | 트랜잭션 내에서 일관된 스냅샷 기준으로 읽기 | 트랜잭션 내에서만 사용 가능하며, 완전한 일관성을 보장한다. | ||
Read Concern은 '읽은 데이터가 어느 시점 기준으로 보장되는가', 즉 데이터의 일관 성 수준을 정의한다. 몽고DB 클라이언트는 기본적으로 local 수준을 사용하여 해당 노드의 메모리에 반영된 최신 데이터를 읽어온다. local 수준보다 안정적으로 읽을 필요가 있는 경우에는 majority 수준을, 데이터 격리가 필요한 경우에는 snapshot 수준을 사용해야 한다.
mjority 쓰기 보장은 커밋된 데이터의 일관된 스냅숏을 제공한다. majority와 snapshot 읽기 수준을 보장하기 위해서는 해당 트랜잭션이 반드시 majority 쓰기 보장 수준으로 커밋되어야 한다. 만약 트랜잭션이 이보다 낮은 수준의 쓰기 보장으로 커밋되면, 시스템은 읽기 작업이 과반수의 승인을 받은 데이터에 접근한다는 것을 보장할 수 없다.
Read Preference
| Read Preference | 설명 | 특징 | ||
| primary | 항상 프라이머리 노드에서 읽기 | 기본값으로 가장 최신 데이터를 읽을 수 있지만 부하가 집중될 수 있다. | ||
| primaryPreferred | 프라이머리 우선, 없으면 세컨더리 노드에서 읽기 | primary 수준보다 높은 가용성을 보장한다. | ||
| secondary | 항상 세컨더리에서 읽기 | 읽기 부하 분산이 가능하지만 복제 지연이 발생할 수 있다. | ||
| secondaryPreferred | 세컨더리 우선, 없으면 프라이머리 노드에서 읽기 | 읽기 부하 분산이 가능하지만 프라이머리에서 읽을 때보다는 일관성 보장 가능성이 낮을 수 있다. | ||
| nearest | 여러 세컨더리 노드 중 네트워크 지연시간이 가장 적은 노드를 선택하여 읽기 | 네트워크 대기시간을 줄여 빠른 응답 시간을 제공하지만 일관성 보장이 안된다. | ||
Read Concern이 '데이터를 어느 시점 기준으로 읽을 것인가'로 데이터의 일관성 수준을 결정한다면 Read Preference는 '어떤 노드에서 읽을 것인가'인 데이터의 출처 위치를 결정하여 레플리카 셋 환경에서 중요한 역할을 한다. 즉, readConcern이 local이고, readPreference가 secondary로 설정되었다면 쿼리는 세컨더리 노드로 보내지고, 세컨더리 노드의 로컬 메모리 상태를 기준으로 데이터를 읽어 프라이머리 노드보다 복제 지연(Replication Lag)으로 인해 조금 이전 시점의 데이터를 읽어올 수 있다는 점을 주의해야 한다.
몽고DB의 Replication Lag 이슈

레플리카 셋이나 샤딩된 클러스터 환경에서는 Write Concern과 Read Preference 설정이 큰 영향을 미친다. 위 그림처럼 WriteConcern.ACKNOWLEDGED와 ReadPreference.secondaryPreferred 설정이 데이터 조회에 어떤 방식으로 영향을 미치는지 보여준다. 아래처럼 프라이머리와 세컨더리 간의 복제 지연 때문에 세컨더리 노드에서 데이터를 찾지 못하는 문제가 발생한 것을 확인할 수 있다. 이를 코드로 표현하면 아래와 같다.
@Service
public class RequestAddService {
@Autowired
private MongoTemplate mongoTemplate;
@Transactional("mongoTransactionManager")
public ResponseDto addRequest(RequestDto requestDto) {
mongoTemplate.setWriteConcern(WriteConcern.ACKNOWLEDGED);
mongoTemplate.save(requestDto.toEntity());
// -> Primary 노드까지 진행이 된다면 응답을 받는다. -> 세컨더리 노드에는 없을수도 있다.
Query query = Query.query(Criteria.where("_id").is(requestDto.getId()));
ReadPreference readPreference = ReadPreference.secondaryPreferred();
Document doc = mongoTemplate.getDb()
.getCollection("request")
.withReadPreference(readPreference)
.find(new Document("_id", requestDto.getId()))
.first();
if (doc == null) {
System.out.println("Lag 발생: Secondary에서 데이터 조회 실패");
}
return new ResponseDto(requestDto.getId());
}
}
위 코드처럼 프라이머티와 세컨더리 간 복제가 완료되지 않은 상황에서 일관성이 중요하다면 기대한 결과를 얻지 못할 가능성이 생길 수 있다. 즉, 세컨더리 노드에서 복제 지연이 발생하면 데이터를 찾지 못하는 상황으로 이어질 수 있다.
Read Preference 조정으로 일관성 보장
// addRequest 비즈니스 로직 내 설정 수정
ReadPreference readPreference = ReadPreference.primary();
// 트랜잭션 시 설정 수정
@Configuration
public class MongoConfig {
@Bean(name = "mongoTransactionManager")
public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
TransactionOptions transactionOptions = TransactionOptions.builder()
// 트랜잭션이 선언되어 있다면, PRIMARY에서 읽어야 한다.
.readPreference(ReadPreference.primary())
.build();
return new MongoTransactionManager(dbFactory, transactionOptions);
}
}
위 코드처럼 서비스 비즈니스 로직 또는 빈으로 트랜잭션 상황에서만 Read Preference 설정 Primary로 설정하여 프라이머리 노드에서 항상 최신의 데이터를 읽을 수 있도록 보장할 수 있다.
애플리케이션을 위한 트랜잭션 제한 조정
몽고DB 트랜잭션에는 두 가지 주요 제한 범주가 있다. 첫 번쨰는 트랜잭션이 실행될 수 있는 시간, 트랜잭션이 락을 획득하려고 대기하는 시간, 모든 트랜잭션이 실행될 최대 길이를 제어하는 것과 관련 있다. 두 번째 범주는 몽고DB oplog 항목과 개별 항목에 대한 크기 제한과 관련 있다.
시간 제한
트랜잭션의 최대 실행 시간은 기본적으로 1분 이하다. 이는 mongod 인스턴스 레벨에서 transactionLifetimeLimitSeconds에 의해 제어되는 제한을 수정해 증가시킬 수 있다. 샤드 클러스터(sharded cluster)의 경우 모든 샤드 레플리카 셋 멤버에 매개변수를 설정해야 한다.
트랜잭션의 작업에 필요한 락을 획득하기 위해 트랜잭션이 대기하는 최대 시간은 기본적으로 5밀리세컨드다. 이 시간 내에 락을 획득할 수 없으면 중단되고, maxTransactionLockRequestTimeoutMillis에 의해 제어되는 제한을 수정해 늘릴 수 있다. 해당 설정값은 0, -1 또는 0보다 큰 숫자로 설정할 수 있다. 0으로 설정한 경우 필요한 모든 락을 즉시 획득할 수 없으면 트랜잭션이 중단된다. -1로 설정하면 작업별 제한 시간이 maxTimeMS에 지정된대로 사용된다. 0보다 큰 숫자는 트랜잭션이 필요한 락을 획득하려고 시도하는 (지정된) 기간으로 해당 시간까지의 대기 시간(wait time)(초)을 구성한다.
Oplog 크기 제한
몽고DB는 트랜잭션의 쓰기 작업에 필요한만큼 oplog 항목을 생성한다. 그러나 각 oplog 항목은 BSON 도큐먼트 크기 제한인 16메가바이트 이하어야 한다.
출처
몽고DB 완벽 가이드: 실전 예제로 배우는 NoSQL 데이터베이스 기초부터 활용까지
마스터링 몽고DB 7.0 : 고급 쿼리 및 아틀라스 등 MongoDB 전문 지식으로 데이터 엑셀런스 달성하기
Spring Boot MongoDB 트랜잭션 도입 실전 가이드
WiredTiger Storage Engine - 공식 문서
MongoDB - 트랜잭션(Transaction) - Isolation Level(격리 수준)
'DB > MongoDB' 카테고리의 다른 글
| MongoDB 인덱싱 (0) | 2025.10.06 |
|---|---|
| MongoDB (0) | 2025.10.06 |
| 몽고DB CRUD (0) | 2025.09.17 |
- Total
- Today
- Yesterday
- 분산 락
- transaction
- redis session
- 비관적 락
- mysql
- nginx configuration
- mdcfilter
- 람다
- nginx
- TDD
- 낙관적 락
- sql
- NeXTSTEP
- spring webflux
- 구름톤 챌린지
- 넥스트스탭
- pessimistic lock
- EKS
- 카프카
- socket
- 트랜잭션
- Redisson
- annotation
- Synchronized
- spring session
- jvm 메모리 구조
- Kafka
- 구름톤챌린지
- Java
- postgresql
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
