두 개의 DB에 동시에 쓰면서 사이드 이펙트를 최소화하는 방법
보통은 데이터를 하나의 데이터베이스에만 저장합니다.
하지만 드물게 두 개의 물리적으로 다른 DB 서버에 동시에 데이터를 써야 할 때가 있습니다.
보통 처음엔 아무 생각 없이 이렇게 한다
참고로 이 글에서 보여주는 코드는 C#과 EF Core 기준이다.
await mDbContext0.SaveChangesAsync();
await mDbContext1.SaveChangesAsync();
대부분은 그냥 이렇게 두 번 저장합니다.
하지만 이건 언젠가 반드시 깨집니다.
첫 번째 커밋이 끝난 직후,
- 두 번째 DB의
SaveChangesAsync()가 validation 오류나 제약 조건 위반으로 실패하거나 - 네트워크가 끊기거나
- 전원 장애가 나면
결국 첫 번째 DB만 반영되고 두 번째는 실패합니다.
즉, 한쪽만 반영된 상태, 다시 말해 데이터 불일치(inconsistency) 가 생깁니다.
오늘 나는 이렇게 해결했다
⚠️ 이 방법은 완벽하지 않습니다.
private async Task crossCommitBestEffortAsync()
{
await using (IDbContextTransaction tx0 = await mDbContext0.Database.BeginTransactionAsync())
await using (IDbContextTransaction tx1 = await mDbContext1.Database.BeginTransactionAsync())
{
// best-effort attempt to make two independent DB commits look atomic
// still unsafe if:
// 1) tx0.CommitAsync() succeeds, and
// 2) power failure happens before tx1.CommitAsync()
try
{
await mDbContext0.SaveChangesAsync();
await mDbContext1.SaveChangesAsync();
await tx0.CommitAsync();
await tx1.CommitAsync();
}
catch
{
await tx0.RollbackAsync();
await tx1.RollbackAsync();
throw;
}
}
}
이 코드는 각 DB에 별도의 트랜잭션을 열고,
둘 다 성공적으로 저장되었을 때만 커밋을 시도합니다.
이전 코드와 뭐가 다를까?
기존의 아무 생각 없이 짠 방식(SaveChangesAsync() 두 번)에서는
첫 번째 DB 커밋이 성공하고, 두 번째 DB에서 예외가 나면
이미 커밋된 데이터를 되돌릴 방법이 없습니다.
하지만 위 코드에서는
- 둘 중 하나라도
SaveChangesAsync()나CommitAsync()에서 실패하면
두 트랜잭션 모두 롤백되도록 보장합니다. - 즉, 정상적인 코드 실행 흐름에서는 두 DB 모두 커밋되거나 모두 롤백됩니다.
이건 "최선의 시도(best effort)" 방식입니다 — 운영체제나 전원이 살아 있는 한, 둘 다 같은 상태로 끝나도록 노력합니다.
그래도 완벽하지 않은 이유
문제는 물리적 장애입니다.
즉, 코드 레벨에서는 방어했지만, 시스템 레벨에서는 여전히 취약합니다.
예를 들어 이런 순서로 일이 벌어지면 망합니다 👇
tx0.CommitAsync()성공- 전원 장애 or 프로세스 크래시
tx1.CommitAsync()호출되지 않음
이 경우, DB0은 이미 커밋된 상태고 DB1은 커밋되지 않았습니다.
즉, 두 DB의 상태가 달라집니다.
이건 코드로 막을 수 없습니다.
두 DB가 물리적으로 독립된 서버이기 때문입니다.
그래서 미션 크리티컬한 곳에는 부적합
이 방식은 커밋 간의 시간차가 아주 작습니다.
하지만 작다는 건 "거의 동시에"일 뿐, 절대 동시에가 아닙니다.
즉,
첫 번째 커밋이 끝나고 두 번째 커밋을 하기 전 0.001초 동안 정전이 나면, 데이터는 불일치 상태로 남습니다.
그래서 미션 크리티컬한 트랜잭션(예: 결제, 정산, 주문 처리 등) 에서는 절대 써서는 안 됩니다.
그럼 왜 나는 이렇게 썼냐면
이 코드는 내부 개발용 툴에서 사용된 패턴입니다.
즉, 일반 유저가 사용하는 서비스는 아니었죠.
문제가 생길 확률도 극히 낮고,
설사 문제가 생겨도 그 순간 내부 개발자가 툴을 직접 다루고 있어서
이상한 상태가 생기면 바로 눈치챌 수 있었습니다.
즉, 리스크가 매우 낮은 환경이라 이렇게 써도 괜찮았습니다.
제대로 하려면 MQ를 써야 한다
안전하게 처리하려면 Message Queue(MQ) 를 써야 합니다.
단, 단순히 "DB에 먼저 확정 반영하고 그 결과를 큐에 남기는 방식"은 여전히 위험합니다.
DB 커밋 직후 전원이 나가면, 큐에 메시지가 남지 않아 재처리조차 불가능하기 때문입니다.
그래서 보통은 DB에 직접 반영하지 않고,
먼저 모든 업데이트 요청을 큐에 넣은 뒤
큐 컨슈머가 DB0, DB1을 차례로 갱신하도록 설계합니다.
이렇게 하면 프로듀서(생산자)는 단 하나의 작업 — 메시지 전송 — 만 수행하고,
장애가 나더라도 큐에 남은 메시지를 통해 항상 재처리할 수 있습니다.
물론 중복 반영 방지나 pre-validation 같은 보완은 필요하지만,
적어도 데이터 불일치로 망하지는 않습니다.
단, 큐 컨슈머가 두 번째 DB에 반영할 때
validation 오류나 비즈니스 제약 때문에 정상적으로 저장할 수 없는 경우도 있습니다.
이런 경우에는 첫 번째 DB에 이미 반영된 데이터를
보상 트랜잭션(compensating transaction) 으로 되돌리는 절차가 필요합니다.
즉, 큐 기반 구조라 해도 완전히 자동은 아니며,
데이터 정합성을 유지하기 위한 별도의 롤백 로직이 함께 설계되어야 합니다.
물론 메시지 큐를 도입한다는 건
결국 또 하나의 프로그램을 돌리고, 또 하나의 저장소를 관리해야 한다는 뜻입니다.
운영 레이어가 하나 더 생기고,
문제가 생기면 SQL처럼 바로 쿼리로 들여다볼 수도 없어
디버깅 난이도도 확 올라갑니다.
즉, 안정성은 올라가지만 시스템은 그만큼 복잡해집니다.
그래서 규모가 작거나, 장애 발생 시 수동 대응이 가능한 내부 시스템이라면
굳이 MQ까지 도입하지 않아도 됩니다.
하지만 안정성이 최우선인 서비스라면 결국 이 방향으로 가야 합니다.
처음 시작할 때는 복잡한 분산 MQ보다
Rebus + SQL 조합을 추천합니다.
설정도 간단하고, 트랜잭션 처리도 깔끔하게 지원합니다.
이전에 관련해서 자세히 다룬 글이 있으니 참고하세요 👇
👉 간단한 메시지 큐는 Rebus로
참고: MSDTC는 온프레미스에서만
윈도우 환경에서 자체 서버를 돌린다면
MSDTC (Microsoft Distributed Transaction Coordinator) 로
여러 DB를 완전한 분산 트랜잭션으로 묶을 수 있습니다.
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
await mDbContext0.SaveChangesAsync();
await mDbContext1.SaveChangesAsync();
scope.Complete();
}
이건 완전히 atomic합니다.
하지만 Azure SQL Database에서는 MSDTC가 지원되지 않습니다.
클라우드 환경에서는 결국
이런 "best effort"나 "queue 기반 보상 트랜잭션" 외엔 방법이 없습니다.
결론
- 그냥
SaveChangesAsync()두 번 부르면 언젠가 문제 생긴다. crossCommitBestEffortAsync()는 정상 시나리오에서는 일관되게 동작하지만, 전원 장애 등 물리적 장애에는 여전히 취약하다.- 미션 크리티컬 시스템에는 절대 부적합하다.
- 안전하게 하려면 Queue 기반 설계로 가야 한다.
- MSDTC는 온프레미스에서만 동작한다.
결국 "commit 두 번"은 사람을 배신하지만, "queue"는 시스템을 구한다. 😏
멀티 DB 트랜잭션이 어려운 이유는 결국 멀티스레딩과 동시성 제어의 본질적인 복잡함 때문이다.
관련 내용은 아래 PopeTV 영상 두 개를 보면 더 잘 이해할 수 있다 👇
🎥 멀티스레딩 마스터하기: 10년의 여정 (PopeTV)
🎥
바람직한 멀티스레딩 구조 (PopeTV)
제대로 대우받는 개발자 | 부족한 컴공지식 배우기 | MIT급 컴공인강
최저임금으로 고통받는 일회성 프로그래머는 그만! POCU 아카데미가 올해 연봉협상을 책임지겠습니다!