PostgreSQL에서 MongoDB 전환 고려
- MongoDB도 버전 4.0부터 다중 문서 ACID 트랜잭션을 지원하지만, RDBMS만큼 강력하지는 않으며 몇 가지 제약(예: 한 트랜잭션 당 1,000개 문서 제한)이 존재함
- ✅ user table을 한 번에 작업하거나 하는 일이 없기 때문에 상관없음
- MongoDB의 Transaction은 기본적으로 timeout 시간이 1분임.
- ???
- CTE, Window Function 등 특정 기능이 없음
- ✅ 쓸 일 없음 (ORM, ODM만 사용)
- Tableau, Power BI와 같은 대부분의 BI(Business Intelligence) 도구 및 데이터 분석 도구들은 SQL 기반의 RDBMS에 대한 지원이 훨씬 강력합니다. MongoDB용 SQL 브릿지가 존재하지만 기능이 제한적이고 설정이 복잡할 수 있음
- ❌ 나중에 데이터 분석시에 문제가 생길 수 있음
- Join은 RDBMS에 비해 50~130배까지 느려질 수 있음
- ❌ 고려해볼만한 문제
- 데이터 영속성 보장(Durability)과 Write Concern: MongoDB는 저널링(Journaling)을 통해 데이터 영속성을 보장하지만, 기본 설정에서는 데이터가 메모리 버퍼에 기록된 후 바로 응답함. 만약 데이터가 디스크 저널 파일에 동기화되기 전(기본 100ms 간격)에 서버가 다운되면 해당 데이터는 유실될 수 있음. 이를 방지하려면
write concern
옵션에서j: true
를 설정하여 디스크 저널에 기록된 것을 확인한 후에 응답을 받도록 해야 함. 이는 데이터 안정성을 높이지만 쓰기 지연 시간을 증가시킬 수 있음.
- ✅ 데이터 유실 가능성을 인지하고, 중요한 데이터에 대해서는 write concern을 조정하여 대응할 수 있음
- MongoDB의 성능 우위 요인: 특정 워크로드에서 RDBMS보다 뛰어난 성능을 보일 수 있으며, 주요 요인은 다음과 같음.
- 데이터 모델링 (임베디드 구조): 관련 데이터를 하나의 문서에 내장하여 RDBMS의 비싼 JOIN 연산을 피하고 데이터 지역성(data locality)을 높여 조회 속도를 향상시킴.
- WiredTiger 스토리지 엔진: 고성능 캐싱과 압축을 통해 디스크 I/O를 최소화하고 스토리지 효율성을 높임.
- 고급 인덱싱: 복합, 다중 키, 텍스트, 지리 공간 등 다양한 인덱싱을 지원하여 복잡한 쿼리 패턴에 맞게 성능을 최적화할 수 있음.
- MongoDB는 B-Tree 인덱스를 사용함.
- ???
Q & A
MongoDB의 Index는 B Tree인데 왜 메모리를 잡아먹는가?
결론부터 말씀드리면, 인덱스가 제 성능을 내기 위해서는 디스크가 아닌 메모리에 있어야 하기 때문입니다.
사용자께서 말씀하신 대로 MongoDB는 인덱스를 위해 B-Tree 자료구조를 사용하며, 이 데이터는 영속성을 위해 디스크에 저장됩니다. 하지만 쿼리 성능을 높이는 것이 인덱스의 주된 목적인데, 쿼리가 실행될 때마다 매번 느린 디스크에서 인덱스를 읽어온다면 인덱스를 사용하는 의미가 퇴색됩니다. 디스크 I/O는 RAM 접근보다 훨씬 느리기 때문입니다.
이 문제를 해결하기 위해 MongoDB의 스토리지 엔진(WiredTiger)은 자주 사용되는 데이터와 **인덱스를 메모리(RAM)에 캐싱(Caching)**합니다. 이를 **워킹셋(Working Set)**이라고 부릅니다.
따라서 인덱스가 OOM(Out of Memory) 문제의 원인이 되는 이유는 다음과 같습니다.
1. 워킹셋과 메모리
MongoDB는 최적의 성능을 위해 현재 애플리케이션이 자주 접근하는 데이터와 인덱스, 즉 '워킹셋'을 모두 RAM에 올려두려고 시도합니다. 만약 이 워킹셋의 크기가 할당된 RAM 크기보다 커지면, MongoDB는 지속적으로 디스크에서 데이터를 읽어오고 오래된 캐시를 비우는 작업을 반복해야 합니다. 이 과정에서 성능이 급격히 저하되고 시스템에 부하가 걸립니다.
2. 인덱스의 메모리 점유
인덱스는 쿼리 속도를 높여주지만, 그 자체로 상당한 메모리 공간을 차지합니다.
- 인덱스의 크기: 컬렉션에 데이터가 많아질수록 인덱스의 크기도 자연스럽게 커집니다.
- 인덱스의 개수: 하나의 컬렉션에 여러 개의 인덱스가 존재하면, 그 인덱스들을 모두 메모리에 유지하기 위해 더 많은 RAM이 필요합니다. MongoDB 공식 문서 (opens in a new tab)에 따르면, 한 컬렉션당 최대 64개의 인덱스를 가질 수 있지만, 실제로는 훨씬 적은 수의 인덱스를 유지하는 것이 일반적입니다.
3. 쓰기 작업(Write Operation)의 영향
인덱스는 읽기 성능을 향상시키지만 쓰기 성능에는 부담을 줍니다. 데이터가 추가, 수정, 삭제될 때마다 해당 데이터가 포함된 모든 인덱스가 함께 업데이트되어야 합니다. 이 업데이트 작업 또한 메모리에서 먼저 발생하므로, 쓰기 작업이 많은 환경에서는 인덱스로 인한 메모리 부하가 더욱 가중됩니다. (MongoDB FAQ: Indexes - How do write operations affect indexes? (opens in a new tab))
결론: OOM Killer가 작동하는 과정
- 데이터가 증가하거나 인덱스가 추가되면서 워킹셋(자주 쓰는 데이터 + 인덱스)의 크기가 물리적인 RAM 용량을 초과하기 시작합니다.
mongod
프로세스는 워킹셋을 메모리에 유지하기 위해 시스템의 가용 메모리를 최대한으로 사용하게 됩니다.- 이로 인해 시스템 전체의 메모리가 부족해지면, Linux 커널의 OOM Killer는 시스템을 보호하기 위해 메모리를 가장 많이 사용하는 프로세스를 강제 종료 대상으로 지목합니다.
- 이때
mongod
프로세스가 가장 많은 메모리를 사용하고 있을 가능성이 높기 때문에 OOM Killer의 대상이 되어 예기치 않게 종료되는 것입니다.
결론적으로, 인덱스는 디스크에 저장되어 영속성을 보장하지만, 실질적인 성능을 내기 위해 RAM에 캐싱되기 때문에 메모리 사용량에 직접적인 영향을 미칩니다. 따라서 인덱스를 설계하고 운영할 때는 쿼리 성능 향상의 이점과 메모리 사용량 증가라는 비용 사이의 균형을 신중하게 고려해야 합니다.
Write Concern을 쓸 때는 어떤 단점이 있고 이걸 해결하기 위해서 어떻게 할 수 있는지? Document 별로 Write Concern을 다르게 설정 가능한지?
MongoDB의 Write Concern은 데이터의 내구성을 보장하는 강력한 기능이지만, 몇 가지 단점과 고려사항이 따릅니다.
Write Concern의 단점: 쓰기 지연 시간(Latency) 증가
Write Concern의 가장 큰 단점은 쓰기 지연 시간(Latency)이 증가한다는 점입니다.
w: 1
: Primary 노드에만 쓰기가 완료되면 즉시 클라이언트에 응답합니다. 속도는 빠르지만, 해당 데이터가 다른 Secondary 노드들로 복제되기 전에 Primary에 장애가 발생하면 데이터가 롤백되어 유실될 수 있습니다.w: "majority"
: 복제본 세트(Replica Set)의 과반수 데이터 보유 노드에 쓰기가 전파되고 저널링까지 완료되는 것을 확인한 후에 응답합니다. 이 방식은 데이터의 내구성을 매우 높여주지만, 여러 노드의 확인을 기다려야 하므로w: 1
에 비해 응답 시간이 길어집니다.
결국, Write Concern은 데이터 내구성(Durability)과 쓰기 성능(Latency) 사이의 트레이드오프(trade-off) 관계에 있습니다. 높은 내구성을 요구하면 성능이 저하되고, 높은 성능을 추구하면 내구성의 손실을 감수해야 합니다.
해결 방안: 요구사항에 맞는 전략적 선택
이 단점은 근본적인 해결책이 있다기보다는, 애플리케이션의 요구사항에 맞춰 적절한 Write Concern 수준을 전략적으로 선택하는 것이 중요합니다.
-
중요 데이터에 대한 높은 내구성 보장:
- 대상: 결제 정보, 사용자 계정, 주문 내역 등 절대 유실되면 안 되는 중요한 데이터.
- 전략: 지연 시간을 감수하더라도
w: "majority"
를 사용하여 데이터가 롤백될 위험을 최소화합니다. 이를 통해 Primary 노드에 장애가 발생해도 데이터의 일관성과 내구성을 보장할 수 있습니다.
-
빠른 쓰기 속도가 중요한 데이터:
- 대상: 실시간 로그, 클릭 스트림, IoT 센서 데이터 등 일부가 유실되어도 시스템 전체에 치명적이지 않은 데이터.
- 전략:
w: 1
을 사용하여 쓰기 지연 시간을 최소화하고 시스템의 처리량을 높입니다.
-
무한 대기 방지를 위한
wtimeout
설정:- 네트워크 문제나 일부 노드의 장애로 인해 지정된 Write Concern 수준을 만족시킬 수 없는 경우, 쓰기 작업이 무한정 대기 상태에 빠질 수 있습니다.
- 이를 방지하기 위해
wtimeout
옵션을 설정할 수 있습니다. 이 옵션은 지정된 시간(밀리초) 내에 Write Concern 확인을 받지 못하면, 쓰기 작업을 타임아웃 처리하고 클라이언트에 오류를 반환합니다. - 주의:
wtimeout
으로 인해 오류가 발생했더라도, 데이터 자체는 일부 노드에 쓰였을 수 있습니다.
Document 별 Write Concern 설정 가능 여부: 가능
네, 가능합니다. 정확히는 'Document 별'이라기보다는 **'쓰기 작업(Write Operation) 별'**로 Write Concern을 다르게 설정할 수 있습니다.
MongoDB 드라이버는 클라이언트, 데이터베이스, 컬렉션 레벨에서 기본 Write Concern을 설정하는 것 외에도, insertOne()
, updateOne()
, deleteMany()
등 개별 쓰기 작업을 실행할 때마다 writeConcern
옵션을 명시하여 해당 작업에만 다른 수준을 적용할 수 있도록 지원합니다.
예를 들어, 평상시 로그는 컬렉션의 기본값인 w: "majority"
로 저장하다가, 특정 고성능 배치를 통해 대량의 비중요 데이터를 삽입할 때는 해당 insertMany
작업에만 w: 1
과 같은 낮은 수준의 Write Concern을 지정하여 처리 속도를 높일 수 있습니다.
// 기본 Write Concern을 따르는 일반적인 쓰기
db.collection.insertOne({ log: "normal operation" });
// 특정 작업에만 다른 Write Concern을 명시적으로 설정
db.collection.insertOne(
{ log: "very important event" },
{ writeConcern: { w: "majority", wtimeout: 5000 } }
);
db.collection.insertMany(
[ { log: "batch job 1" }, { log: "batch job 2" } ],
{ writeConcern: { w: 1, j: false } }
)
단, 한 가지 예외가 있습니다. 여러 문서를 원자적으로 업데이트하는 트랜잭션(Multi-document Transaction) 내에서는 개별 작업에 Write Concern을 설정할 수 없으며, 트랜잭션 전체에 대한 Write Concern을 커밋 시점에 한 번에 설정해야 합니다.
출처:
결론
- user repository에서 write concern을 사용한다면 쓰기 안정성을 부여할 수 있음
- 트랜잭션 및 락경합 관리를 해야할 경우 MongoDB의 트랜잭션을 사용하기 보단 분산락이나 다른 방법으로 구현하는 것이 좋을 것 같음
- 하지만 현재 user entity에 접근 후 작업해야 하는 비즈니스 로직의 특성상 동시성 문제가 발생할 가능성이 매우 희박