MongoDB에서 Read Preference와 Write Concern은 데이터의 일관성, 가용성, 성능 간의 트레이드오프를 조절하는 핵심 설정이다. 특히 레플리카 셋 환경에서 이 설정들은 트랜잭션과 데이터 처리에 직접적인 영향을 미친다
Write Concern – 쓰기 확인 수준
Write Concern은 쓰기 작업이 얼마나 많은 노드에 반영되었는지 확인하는 설정이다. 데이터의 내구성과 성능 간의 균형을 조절한다
Write Concern 옵션 상세
Acknowledged (기본값)
- 동작 방식: Primary 노드가 쓰기 작업을 메모리에 적용하고 즉시 성공 응답을 반환한다
- 장점: 응답 속도가 빠르며 대부분의 경우에 적합한 기본 설정이다
- 단점: Secondary 노드로 복제되기 전 Primary 장애 시 데이터 손실 가능성이 있으며 네트워크 장애 시 데이터 유실 위험이 존재한다
- 사용 사례: 일반적인 CRUD 작업, 성능과 신뢰성이 균형이 필요한 경우
@Configuration
public class MongoConfig {
@Bean
public MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDbFactory) {
MongoTemplate template = new MongoTemplate(mongoDbFactory);
template.setWriteConcern(WriteConcern.ACKNOWLEDGED);
return template;
}
}
Unacknowledged (Fire and Forget)
- 동작 방식: 요청 전송 즉시 성공을 응답하며 실제 저장 여부와 무관하게 성공을 반환한다
- 장점: 가장 빠른 쓰기 성능
- 단점: 데이터 저장 실패 여부를 알 수 없고 데이터 손실 위험이 매우 높다. 실무 서비스에는 적합하지 않다
- 사용 사례: 손실되어도 무방한 로그 데이터나 임시 캐시 데이터로 실무에서는 많이 사용하지 않는 편이다
mongoTemplate.setWriteConcern(WriteConcern.UNACKNOWLEDGED);
Majority (권장)
- 동작 방식: 레플리카 셋의 과반수 노드에 데이터 반영을 대기하며 과반수 합의 후 성공을 응답한다
- 장점: 높은 데이터 일관성을 보장, Primary 장애 시에도 데이터 보존이 가능하며 분산 시스템에서 안정적이다
- 단점: 복제 대기로 인해 레이턴시가 증가하며 네트워크 상태에 따른 성능 변동성이 있다
- 사용 사례: 금융 거래, 중요한 비즈니스 데이터, 데이터 무결성과 일관성이 최우선인 경우
@Service
public class CriticalDataService {
@Autowired
private MongoTemplate mongoTemplate;
public void saveCriticalData(Transaction transaction) {
// Majority 설정으로 안전성 보장
mongoTemplate.setWriteConcern(WriteConcern.MAJORITY);
mongoTemplate.save(transaction);
}
}
레플리카 셋 구성 예시
Primary + 2 Secondary (총 3개 노드) → Majority = 2개 노드에 반영 필요 Primary + 4 Secondary (총 5개 노드) → Majority = 3개 노드에 반영 필요
W1, W2, W3 (숫자 지정)
- 동작 방식: 지정한 N개의 노드에 쓰기 반영을 확인한다. Majority와 유사하지만 노드 수를 직접 지정한다
- 장점: 세밀한 제어가 가능하며 비즈니스 요구사항에 맞춤 설정이 가능하다
- 단점: 노드 수 변경 시 설정 재조정이 필요하며 잘못된 설정 시 성능 저하 될 수 있다
- 사용 사례: 특정 개수의 백업이 필요한 경우나 커스텀 복제 정책이 필요한 경우
// 2개 노드에 반영될 때까지 대기 WriteConcern w2 = WriteConcern.W2; mongoTemplate.setWriteConcern(w2); // 3개 노드에 반영될 때까지 대기 WriteConcern w3 = WriteConcern.W3; mongoTemplate.setWriteConcern(w3);
Journaled
- 동작 방식: 쓰기 작업이 디스크의 저널(Journal) 파일에 기록될 때까지 대기한다
- 저널: 장애 복구를 위한 트랜잭션 로그
- 장점: 서버 장애 시에도 데이터 복구가 가능하며 최고 수준의 내구성을 보장한다
- 단점: 디스크 I/O로 인한 성능 저하와 가능 느린 응답 시간
- 사용 사례: 절대 손실되어선 안 되는 데이터와 감사 로그, 금융 거래 기록등이 있다. 실무에서는 Majority + Journal 조합을 사용한다
WriteConcern journaled = WriteConcern.JOURNALED; mongoTemplate.setWriteConcern(journaled);
저널(Journal) 메커니즘
1. 쓰기 요청 수신 2. 메모리에 변경사항 적용 3. 저널 파일에 기록 (디스크) 4. 실제 데이터 파일에 기록 5. 클라이언트에 응답
Write Concern 비교표
| 옵션 | 응답 시점 | 성능 | 안정성 | 데이터 손실 위험 |
| Unacknowledged | 요청 즉시 | 최고 | 최하 | 매우 높음 |
| Acknowledged | Primary 메모리 | 높음 | 중간 | 중간 |
| Majority | 과반수 노드 | 중간 | 높음 | 낮음 |
| W2/W3 | N개 노드 | 중간 | 높음 | 낮음 |
| Journaled | 디스크 저널 | 낮음 | 최고 | 최소 |
Write Concern 선택 가이드
@Service
public class DataService {
@Autowired
private MongoTemplate mongoTemplate;
// 일반 데이터: Acknowledged (기본값)
public void saveGeneralData(User user) {
mongoTemplate.save(user);
}
// 중요 데이터: Majority
public void saveCriticalData(Order order) {
mongoTemplate.setWriteConcern(WriteConcern.MAJORITY);
mongoTemplate.save(order);
}
// 초중요 데이터: Majority + Journal
public void saveVitalData(Payment payment) {
WriteConcern vital = WriteConcern.MAJORITY.withJournal(true);
mongoTemplate.setWriteConcern(vital);
mongoTemplate.save(payment);
}
// 로그 데이터: Acknowledged (빠른 처리)
public void saveLog(LogEntry log) {
mongoTemplate.setWriteConcern(WriteConcern.ACKNOWLEDGED);
mongoTemplate.save(log);
}
}
레플리카 셋 필수
- 고급 Write Concern 옵션들(w>1, majority 등)은 레플리카 셋 환경에서 진정한 의미를 갖는다. Standalone에서는 대부분의 고급 옵션이 제한적으로만 동작하거나 무시된다
- Standalone 노드에서는 대부분의 옵션이 무시된다
- 프로덕션 환경에서는 반드시 레플리카 셋 구성이 필요하다
실무 적용
- 대부분의 경우 기본값 (Acknowledged)을 사용한다
- 쓰기보다 읽기 트래픽이 많은 시스템에서는 Write Concern 조정 효과가 제한적이다
- 서비스의 성격에 따라 중요한 쓰기 경로에만 Majority를 선택적으로 적용
Read Preference – 읽기 노드 선택
Read Preference는 레플리카 셋 환경에서 읽기 작업이 어떤 노드로 라우팅될지 결정하는 설정이다
Read Preference가 중요한 이유
데이터 일관성 (Consistency)
- 사용자에게 얼마나 최신 데이터를 제공할 것인가
- 복제 지연(Replication Lag)으로 인한 불일치 허용
고가용성 (Availability)
- Primary 노드 장애 시에도 데이터 읽기 가능 여부
- 서비스 무중단 전략
성능 (Performance)
- 노드 간 부하 분산과 네트워크 지연 최소화
글로벌 서비스 (Global Distribution)
- 지역별 분산 노드 활용
- 사용자와 가까운 노드에서 데이터 제공 (CDN 개념)
Read Preference 옵션 상세
Primary (기본값)
- 동작 방식: 모든 읽기 요청을 Primary 노드로 전달하며 항상 최신 데이터를 보장한다
- 장점: 강력한 데이터 일관성과 쓰기 직후 즉시 읽기가 가능하며 예측 가능한 동작을 한다
- 단점: Primary 장애 시 읽기가 불가하여 가용성이 저하되고 Primary에 모든 부하가 집중된다. 스케일 아웃 효과가 없다
- 사용 사례: 은행 시스템, 결제 처리, 이커머스 주문, 실시간 재고 관리
@Service
public class OrderService {
@Autowired
private MongoTemplate mongoTemplate;
public Order getOrder(ObjectId orderId) {
// Primary에서 최신 데이터 조회
Query query = Query.query(Criteria.where("_id").is(orderId));
query.withReadPreference(ReadPreference.primary());
return mongoTemplate.findOne(query, Order.class);
}
}
Secondary
- 동작 방식: 모든 읽기 요청을 Secondary 노드로 분산하고 여러 Secondary 중 랜덤하게 선택한다
- 장점: Primary 부하 분산, 읽기 스케일 아웃 가능, 대용량 조회에 유리하며 일부 노드 장애 시에도 작동하는 높은 가용성
- 단점: 복제 지연으로 인한 약한 일관성과 최신 데이터 보장이 불확실하다. 노드마다 다른 데이터 상태가 가능하다
- 사용 사례: 토계 데이터 조회, 리포트 생성, 대용량 배치 작업, 실시간성이 덜 중요한 데이터
@Service
public class StatisticsService {
@Autowired
private MongoTemplate mongoTemplate;
public List<Statistics> getDailyStatistics(Date date) {
// Secondary에서 통계 데이터 조회 (Primary 부하 방지)
Query query = Query.query(
Criteria.where("date").is(date)
);
query.withReadPreference(ReadPreference.secondary());
return mongoTemplate.find(query, Statistics.class);
}
// 대용량 리포트 생성
public void generateMonthlyReport(int year, int month) {
Query query = Query.query(
Criteria.where("year").is(year)
.and("month").is(month)
);
query.withReadPreference(ReadPreference.secondary());
// 많은 데이터를 Secondary에서 처리
List<Transaction> transactions = mongoTemplate.find(
query, Transaction.class
);
// 리포트 생성 로직...
}
}
Primary Preferred
- 동작 방식: 기본적으로 Primary에서 읽으며 Primary 장애 시 Secondary로 폴백한다
- 장점: 일반적으로 최신 데이터를 제공하고 장애 상황에서도 서비스가 자속되며 유지보수 시에도 읽기가 가능하다
- 단점: 폴백 시 데이터 일관성이 저하되고 Primary → Secondary 전환 시 데이터 불일치가 될 수 있다. 그리고 디버깅이 복잡하다
주의사항
// 잘못된 사용 예시
@Transactional
public void processData(ObjectId id) {
// Primary에 저장
Data data = new Data();
mongoTemplate.save(data);
// Primary Preferred로 조회
// Primary 장애 시 Secondary에서 조회
// → 방금 저장한 데이터가 아직 복제 안 됨
Query query = Query.query(Criteria.where("_id").is(data.getId()));
query.withReadPreference(ReadPreference.primaryPreferred());
Data found = mongoTemplate.findOne(query, Data.class);
// found가 null일 수 있음
}
- 사용 사례
- 글로벌 서비스 (다중 지역 배포), 무중단 서비스가 최우선
- Primary 장애 시 최소 기능이라도 유지가 필요한 경우, 약간의 데이터 불일치를 허용한는 경우
@Service
public class UserProfileService {
@Autowired
private MongoTemplate mongoTemplate;
// 사용자 프로필 조회 (Primary Preferred)
public UserProfile getUserProfile(ObjectId userId) {
Query query = Query.query(Criteria.where("_id").is(userId));
query.withReadPreference(ReadPreference.primaryPreferred());
return mongoTemplate.findOne(query, UserProfile.class);
}
}
Secondary Preferred
- 동작 방식: 기본적으로 Secondary에서 읽으며 모든 Secondary 장애 시 Primary로 볼팩한다
- 장점: Primary 부하 최소화, Secondary 활용 극대화, 일부 Secondary 장애에도 계속 작동한다
- 단점: 복제 지연 (Replication Lag)이 항상 존재하며 데이터 일관성이 가장 약하다
- 사용 사례: 대규모 통계 처리, 로그 분석, 모니터링 시스템, 실시간성이 매우 낮은 데이터
@Service
public class AnalyticsService {
@Autowired
private MongoTemplate mongoTemplate;
// 대용량 로그 분석 (Secondary Preferred)
public AnalyticsResult analyzeUserBehavior(Date startDate, Date endDate) {
Query query = Query.query(
Criteria.where("timestamp")
.gte(startDate)
.lte(endDate)
);
// Secondary에서 처리하여 Primary 부하 방지
query.withReadPreference(ReadPreference.secondaryPreferred());
List<UserLog> logs = mongoTemplate.find(query, UserLog.class);
// 분석 로직...
return new AnalyticsResult(logs);
}
}
Nearest
- 동작 방식: 네트워크 레이턴시가 가장 낮은 노드를 선택하고 그 노드가 Primary인 수도, Secondary일 수도 있다. MongoDB 드라이버가 주기적으로 핑 테스트를 하여 결정한다
- 장점: 최소 응답 시간을 보장하고 글로벌 분산 환경에서 효과적이다. 지역별 최적의 성능을 보장한다
- 단점: 어떤 노드에 접근할지 예측이 불가하고 데이터 일관성 보장이 어렵다. 노드마다 다른 상태의 데이터 반환 가능성이 있다
- 사용 사례: 글로벌 CDN 서비스, 지역별 콘텐츠 서비스, 레이턴시가 최우선인 경우
@Service
public class ContentDeliveryService {
@Autowired
private MongoTemplate mongoTemplate;
// 사용자와 가장 가까운 노드에서 콘텐츠 제공
public Content getContent(ObjectId contentId) {
Query query = Query.query(Criteria.where("_id").is(contentId));
// 가장 빠른 응답을 제공하는 노드 선택
query.withReadPreference(ReadPreference.nearest());
return mongoTemplate.findOne(query, Content.class);
}
}
글로벌 배포 예시
서울 리전: Primary + Secondary 도쿄 리전: Secondary 싱가포르 리전: Secondary 한국 사용자 → 서울 노드 (레이턴시 10ms) 일본 사용자 → 도쿄 노드 (레이턴시 5ms) 싱가포르 사용자 → 싱가포르 노드 (레이턴시 3ms)
Read Preference 비교표
| 옵션 | 읽기 노드 | 일관성 | 가용성 | 부하분산 |
| Primary | Primary만 | 강함 | 낮음 | 없음 |
| Secondary | Secondary만 | 약함 | 높음 | 우수 |
| Primary Preferred | Primary 우선 | 중간 | 높음 | 제한적 |
| Secondary Preferred | Secondary 우선 | 약함 | 매우 높음 | 우수 |
| Nearest | 가장 가까운 노드 | 약함 | 높음 | 우수 |
Replication Lag
Write Concern과 Read Preference 부적절한 조합으로 인해 실무에서 데이터 불일치 문제가 발생할 수 있다
@Service
public class RequestService {
@Autowired
private MongoTemplate mongoTemplate;
@Transactional("mongoTransactionManager")
public ResponseDto addRequest(RequestDto requestDto) {
// 1. ACKNOWLEDGED로 저장 (Primary까지만 반영)
mongoTemplate.setWriteConcern(WriteConcern.ACKNOWLEDGED);
Request savedRequest = mongoTemplate.save(requestDto.toEntity());
// 2. Secondary에서 조회 시도
Query query = Query.query(
Criteria.where("_id").is(savedRequest.getId())
);
// 3. Replication Lag 문제 발생 지점
ReadPreference readPreference = ReadPreference.secondaryPreferred();
Document doc = mongoTemplate.getDb()
.getCollection("requests")
.withReadPreference(readPreference)
.find(new Document("_id", savedRequest.getId()))
.first();
if (doc == null) {
// Replication Lag 발생!
// Primary에는 저장되었지만 Secondary에는 아직 복제 안 됨
throw new DataNotFoundException(
"Secondary에서 데이터 조회 실패 - Replication Lag"
);
}
return new ResponseDto(savedRequest.getId());
}
}
문제 발생 시나리오
1. ACKNOWLEDGED로 데이터 저장 → Primary 메모리에만 반영 2. secondaryPreferred()로 즉시 조회 → Secondary는 아직 복제 전 3. 데이터를 찾을 수 없음 → Replication Lag 발생
해결 방법 1 – Write Concern을 Majority로 변경
@Service
public class RequestService {
@Autowired
private MongoTemplate mongoTemplate;
@Transactional("mongoTransactionManager")
public ResponseDto addRequestSolution1(RequestDto requestDto) {
// Majority로 변경: 과반수 노드에 반영될 때까지 대기
mongoTemplate.setWriteConcern(WriteConcern.MAJORITY);
Request savedRequest = mongoTemplate.save(requestDto.toEntity());
// 이제 Secondary에 어느 정도 복제가 보장됨
Query query = Query.query(
Criteria.where("_id").is(savedRequest.getId())
);
ReadPreference readPreference = ReadPreference.secondaryPreferred();
Document doc = mongoTemplate.getDb()
.getCollection("requests")
.withReadPreference(readPreference)
.find(new Document("_id", savedRequest.getId()))
.first();
if (doc != null) {
return new ResponseDto(savedRequest.getId());
}
throw new DataNotFoundException("데이터 조회 실패");
}
}
- 장점: 데이터 일관성을 보장하고 Secondary에서 안전하게 읽기 가능
- 단점: 모든 요청에 과반수 합의 대기로 인한 오버헤드와 레이턴시 증가
해결 방법 2 – Read Preference를 Primary Preferred로 변경
@Service
public class RequestService {
@Autowired
private MongoTemplate mongoTemplate;
@Transactional("mongoTransactionManager")
public ResponseDto addRequestSolution2(RequestDto requestDto) {
// ACKNOWLEDGED 유지 (빠른 쓰기)
mongoTemplate.setWriteConcern(WriteConcern.ACKNOWLEDGED);
Request savedRequest = mongoTemplate.save(requestDto.toEntity());
// primaryPreferred()로 변경: Primary에서 읽기
Query query = Query.query(
Criteria.where("_id").is(savedRequest.getId())
);
ReadPreference readPreference = ReadPreference.primaryPreferred();
Document doc = mongoTemplate.getDb()
.getCollection("requests")
.withReadPreference(readPreference)
.find(new Document("_id", savedRequest.getId()))
.first();
if (doc != null) {
return new ResponseDto(savedRequest.getId());
}
throw new DataNotFoundException("데이터 조회 실패");
}
}
- 장점: 쓰기 성능유 유지되며 방금 저장한 데이터 즉시 조회 가능
- 단점: Primary에 부하가 집중된다
해결 방법 3 – 트랜잭션 설정 활용 (권장)
@Configuration
public class MongoTransactionConfig {
@Bean
public MongoTransactionManager mongoTransactionManager(
MongoDatabaseFactory dbFactory) {
TransactionOptions options = TransactionOptions.builder()
.readPreference(ReadPreference.primary()) // 트랜잭션 내 읽기는 Primary
.readConcern(ReadConcern.MAJORITY) // 읽기 일관성 보장
.writeConcern(WriteConcern.MAJORITY) // 쓰기 일관성 보장
.build();
MongoTransactionManager manager = new MongoTransactionManager(dbFactory);
manager.setOptions(options);
return manager;
}
}
@Service
public class RequestService {
@Autowired
private MongoTemplate mongoTemplate;
// 트랜잭션 사용 시 자동으로 Primary에서 읽기
@Transactional("mongoTransactionManager")
public ResponseDto addRequestSolution3(RequestDto requestDto) {
// 저장
Request savedRequest = mongoTemplate.save(requestDto.toEntity());
// 트랜잭션 내에서는 자동으로 Primary에서 조회
Query query = Query.query(
Criteria.where("_id").is(savedRequest.getId())
);
Request found = mongoTemplate.findOne(query, Request.class);
if (found != null) {
return new ResponseDto(found.getId());
}
throw new DataNotFoundException("데이터 조회 실패");
}
}
- 장점: 범용적인 해결쳑이며 트랜잭션의 ACID 속성을 활용한다. 그리고 명시적 설정이 필요하지 않다
- 단점: 트랜잭션 오버헤드
해결 방법 비교
| 방법 | 쓰기 성능 | 읽기 일관성 | 복잡도 | 권장도 |
| Majority Write | 낮음 | 높음 | 낮음 | ⭐⭐⭐ |
| Primary Preferred | 높음 | 높음 | 낮음 | ⭐⭐⭐⭐ |
| Transaction Config | 중간 | 매우 높음 | 중간 | ⭐⭐⭐⭐⭐ |
핵심 정리
Write Concern:
- 데이터 안정성과 성능의 트레이드 오프
- 기본값 (Acknowledged)이 대부분의 경우 적합
- 중요 데이터는 Majority 사용
- 초중요 데이터는 Majority + Journal 조합
Read Preference
- 일관성, 가용성, 성능의 균형 조절
- 기본값 (Primary)이 안전
- 통계/분석은 Secondary 활용
- Replication Lag 항상 고려