본문 바로가기
🚀 데브 노트 (IT 지식 & 팁)

DB 부하를 줄이는 마법? 레플리케이션(Replication) 읽기/쓰기 분리와 복제 지연(Lag) 트러블슈팅

by 데브프리 2026. 8. 8.

안녕하세요, 여러분의 든든한 IT 길잡이 데브프리입니다.

 

서비스가 성장하고 사용자가 늘어나면 가장 먼저 비명을 지르는 곳은 어디일까요? 바로 데이터베이스(DB)입니다. 웹 서버(WAS)는 트래픽이 몰리면 단순히 서버를 여러 대 띄워서(Scale-out) 로드밸런서로 묶어버리면 그만입니다. 하지만 모든 데이터가 최종적으로 저장되는 DB는 함부로 여러 대를 띄울 수 없습니다. 데이터의 일관성이 깨지기 때문이죠.

 

이때 백엔드 아키텍처에서 DB의 CPU가 100%를 치는 것을 막기 위해 가장 먼저 도입하는 실무 표준 기술이 바로 데이터베이스 레플리케이션(Replication)과 읽기/쓰기 분리(Read/Write Splitting)입니다.

 

하지만 이 기술을 스프링 부트(Spring Boot)에 연동하고 실서버에 배포하는 순간, 로컬에서는 상상도 못 했던 기묘한 에러들이 터지기 시작합니다. 오늘은 실무에서 레플리케이션 도입 시 100% 겪게 되는 '복제 지연(Replication Lag)' 문제를 객관적으로 분석하고, 이를 완벽하게 해결하는 베스트 프랙티스를 짚어드리겠습니다.

 

1. 레플리케이션(Replication)과 읽기/쓰기 분리의 원리

대부분의 웹 서비스는 트래픽의 비율이 극단적으로 치우쳐 있습니다. 사용자가 글을 작성(Write)하는 횟수보다, 다른 사람의 글을 조회(Read)하는 횟수가 압도적으로 많죠. 보통 Write와 Read의 비율은 1:9 또는 2:8에 달합니다.

그래서 DB를 역할에 따라 쪼개는 전략을 사용합니다.

  • Master (Writer): 오직 INSERT, UPDATE, DELETE 등 쓰기 작업만 전담합니다.
  • Slave / Replica (Reader): Master의 데이터를 그대로 복사(동기화)해온 뒤, SELECT 조회 작업만 전담합니다.

스프링 부트 환경에서는 AbstractRoutingDataSource를 활용하여 트랜잭션의 속성에 따라 쿼리를 분기합니다.

로직에 @Transactional(readOnly = true)가 붙어있으면 Replica DB로 쿼리를 날리고, readOnly = true가 없으면 Master DB로 쿼리를 날리도록 세팅하여 90%에 달하는 조회 트래픽을 Replica 서버들로 우아하게 분산시키는 것이 핵심입니다.

 

2. 실전 트러블슈팅: "방금 가입했는데 왜 로그인이 안 되죠?"

아키텍처 설정을 마치고 실서버에 배포했습니다. 그런데 배포 직후, 사용자들로부터 황당한 CS(고객 문의)가 인입되기 시작합니다.

  • "게시글을 작성하고 확인을 눌렀는데, 제 글이 안 보여요. 3초 뒤에 새로고침 하니까 그제야 보이네요."
  • "회원가입을 완료하자마자 자동으로 로그인 화면으로 넘어갔는데, '존재하지 않는 회원'이라고 뜹니다!"

백엔드 서버의 에러 로그를 확인해 보면 아래와 같은 붉은 글씨가 찍혀 있습니다.

Java

org.springframework.security.core.userdetails.UsernameNotFoundException: User not found with email: devfree@example.com
    at com.example.service.CustomUserDetailsService.loadUserByUsername(CustomUserDetailsService.java:25)

분명히 회원가입 로직에서 userRepository.save()를 통해 DB에 데이터를 잘 넣었는데, 바로 다음 줄이나 다음 화면에서 조회해 보면 데이터가 없다고 에러가 터지는 상황입니다.

왜 터졌을까? (복제 지연 현상의 덫)

문제의 원인은 물리적인 '복제 지연(Replication Lag)'입니다.

 

Master DB에 데이터가 기록(INSERT)되면, 그 데이터가 네트워크를 타고 Replica DB로 복사되기까지 아주 미세한 시간 차이가 발생합니다. 보통 수십 밀리초(ms)에서 길면 1~2초까지 걸리기도 하죠. 이를 비동기 복제(Asynchronous Replication)의 한계라고 부릅니다.

  1. 사용자가 회원가입을 완료합니다. (Master DB에 저장 완료)
  2. 클라이언트(프론트엔드)가 "가입 완료되었으니, 내 프로필 정보를 줘!"라며 즉시 조회 API를 호출합니다.
  3. 이 조회 API는 @Transactional(readOnly = true)가 붙어있어 Replica DB를 바라봅니다.
  4. 하지만 Master DB의 데이터가 Replica DB로 아직 복사되지 않았습니다! (지연 발생)
  5. Replica DB는 "그런 회원 없는데요?"라며 에러를 뱉어냅니다.

데이터베이스 복제 지연(Replication Lag) 현상 비유
Master DB에 기록된 데이터가 Replica DB로 동기화되기까지 물리적인 시간이 소요되며, 이 짧은 틈에 조회를 시도하면 데이터를 찾지 못하는 복제 지연 현상이 발생합니다.

 

3. 복제 지연(Replication Lag)을 해결하는 실무 베스트 프랙티스

이 현상을 해결하기 위해 "조회하기 전에 Thread.sleep(1000)으로 1초 기다리자" 같은 꼼수를 부려서는 절대 안 됩니다. 사용자 경험(UX)을 훼손하지 않으면서 정합성을 맞추는 2가지 베스트 프랙티스를 적용해야 합니다.

① 쓰기 직후의 조회는 Master DB를 바라보게 강제하기

가장 확실하고 많이 쓰이는 방법입니다. 데이터가 방금 막 수정/생성된 직후라면, 그 데이터에 한해서는 Replica가 아닌 Master DB에서 직접 읽어오도록 라우팅 규칙을 예외 처리하는 것입니다.

 

스프링 부트 내부에서 동일한 트랜잭션으로 묶어버리는 것이 가장 간단한 구현법입니다. 만약 글 작성 직후 저장된 글을 바로 조회하여 반환해야 한다면, 조회 메서드에 readOnly = true를 거는 대신 쓰기 트랜잭션 내부에 포함시켜 Master를 바라보게 유지하세요.

Java

@Transactional // readOnly를 걸지 않음으로써 전체 로직이 Master DB를 향함
public PostResponse createPost(PostRequest request) {
    // 1. Master에 쓰기
    Post savedPost = postRepository.save(request.toEntity()); 
    
    // 2. 쓰기 트랜잭션 안에서 조회하므로 지연 없이 Master에서 안전하게 읽어옴
    return postRepository.findById(savedPost.getId()) 
            .map(PostResponse::from)
            .orElseThrow(); 
}

② 로컬 캐시(Redis 등) 활용하기 (CQRS 패턴의 일부)

Master를 조회하게 하는 것은 안전하지만, 결국 Master의 부하를 줄이겠다는 초기 목적과 충돌할 수 있습니다.

따라서 대규모 시스템에서는 쓰기 작업이 성공하면 그 최신 데이터를 DB에만 넣는 것이 아니라 Redis 같은 빠른 인메모리 캐시에도 즉시 업데이트(Write-Through) 해둡니다.

 

이후 조회가 들어오면 일단 캐시를 먼저 확인합니다. 캐시에 방금 가입한 최신 데이터가 존재하므로 Replica DB의 동기화가 늦어져도 사용자에게는 아무런 지연 없이 최신 상태를 반환할 수 있습니다.

 

💡 [데브프리의 심층 면접 꿀팁: 프론트엔드가 '별도의 GET API'로 조회하면 어떻게 될까요?]

면접관이 "쓰기 트랜잭션 안에서 조회하면 안전하지만, 프론트엔드가 POST(저장) 직후 완전히 별도의 HTTP 요청으로 GET(조회) API를 호출하면 어떻게 되나요?"라고 묻는다면 100점짜리 압박 질문입니다!

1번에서 배운 readOnly 꼼수는 '하나의 트랜잭션(하나의 API)' 안에서 벌어질 때만 유효합니다. 만약 회원가입 API(POST) 응답이 끝난 직후 프론트엔드가 내 정보 조회 API(GET)를 쏘게 되면, 이 GET API는 얄짤없이 Replica로 향하게 되어 또다시 복제 지연 에러를 맞게 됩니다.

이를 완벽하게 방어하려면 1) POST API의 응답값으로 생성된 데이터를 아예 꽉 채워서 내려주어 프론트엔드가 GET API를 연달아 호출할 필요가 없도록 API를 설계하거나, 2) JWT나 세션에 '최근 쓰기 시간'을 기록하여 3~5초 내의 요청은 무조건 Master로 라우팅되도록 커스텀 Interceptor를 구현(Sticky Master 패턴)해야 한다는 점을 꼭 어필하세요. 이 대답 하나면 아키텍처의 한계를 꿰뚫는 시니어 개발자로 인정받을 수 있습니다!

 

4. 전문가의 확고한 결론 (Verdict)

DB 레플리케이션(Master-Replica), 실무에 언제 도입하고 언제 피해야 할까요?

  • 👍 확실한 도입 타이밍: 애플리케이션 서버를 여러 대 띄웠음에도 불구하고, DB 자체의 CPU 사용률이 70~80%를 지속적으로 넘기며 전체 시스템의 병목(Bottleneck) 구간이 DB로 확정되었을 때 도입해야 합니다. 커머스(쇼핑몰), 커뮤니티, 콘텐츠 플랫폼 등 '읽기(Read) 트래픽이 쓰기(Write) 트래픽보다 압도적으로 많은 서비스'에서 가장 드라마틱한 성능 향상과 스케일 아웃 효과를 얻을 수 있습니다.
  • 🛑 도입을 피해야 할 때: 증권사 시스템이나 실시간 뱅킹, 포인트 결제 시스템처럼 데이터의 '강한 일관성(Strong Consistency)'이 1ms의 오차도 없이 완벽하게 보장되어야 하는 서비스라면 비동기 레플리케이션을 통한 분산 조회를 피해야 합니다. 복제 지연으로 인해 옛날 금액(Stale Data)을 읽고 결제가 승인되는 대참사가 발생할 수 있습니다. 이런 도메인에서는 차라리 고성능의 단일 DB로 스케일 업(Scale-up)을 진행하거나, 복제 동기화를 기다리는 동기식(Synchronous) 클러스터링을 구성해야 합니다.

레플리케이션은 DB 부하를 마법처럼 줄여주는 훌륭한 아키텍처지만, 그 이면에는 '최종 일관성(Eventual Consistency)'이라는 트레이드오프가 존재합니다. 오늘 분석한 복제 지연의 원리를 완벽하게 통제하여, 트래픽 폭주에도 흔들리지 않는 견고한 백엔드 시스템을 설계하시길 바랍니다.

 

항상 팩트에 기반한 객관적인 실무 가이드로 여러분의 삽질 시간을 줄여드리겠습니다. 다음 포스팅에서도 개발자들에게 유용한 아키텍처 분석으로 찾아뵙겠습니다!

 

👉 실서버 뻗기 전에 꼭 확인하세요! 내 서버 스펙에 맞는 최적의 DB 커넥션 풀 개수 계산기 [링크]