본문 바로가기
Back-end/SpringBoot

배치가 밀릴 때 발생할 수 있는 락(Lock) 이슈와 해결 방안

by backend 개발자 지망생 2025. 12. 1.

데이터가 누적되어 배치가 지연되면 단순히 처리가 늦게 끝나는 문제에 그치지 않는다. 트랜잭션을 길게 점유하면서 운영 DB(OLTP)를 마비시키는 락 상황이 발생할 수 있다. 이에 대한 원인과 해결책을 정리했다.

1. 문제 원인: 락 발생 메커니즘

일반적인 배치는 조회(READ), 가공(PROCESS), 저장(WRITE)의 흐름을 가진다. 여기서 병목과 문제를 유발하는 단계는 주로 저장(WRITE) 단계다.

  • Row Lock (행 잠금) 배치가 데이터를 수정(UPDATE)하거나 삭제(DELETE)하는 순간, 해당 행들은 커밋되기 전까지 잠긴다. 만약 데이터가 밀려 1만 건을 한 번에 업데이트한다면 트랜잭션이 끝날 때까지 1만 건의 데이터가 잠겨 있게 된다. 이때 실제 사용자가 그중 하나의 데이터를 조회하거나 수정하려 하면 무한 대기(Lock Wait) 상태에 빠지거나 타임아웃 에러가 발생한다.
  • Gap Lock (갭 락) MySQL InnoDB 등의 경우 범위 조건으로 업데이트를 수행할 때 데이터가 없는 사이 공간까지 락을 걸 수 있다. 이 경우 배치가 실행되는 동안 새로운 데이터의 입력(INSERT)조차 막힐 위험이 있다.

2. 해결책: 락을 최소화하는 전략

스프링 배치에서는 이러한 문제를 방지하기 위해 트랜잭션을 잘게 쪼개는 전략을 사용한다.

  • 전략 A: Chunk Size(커밋 단위) 조절 가장 중요한 전략으로 스프링 배치의 핵심 기능이다. 데이터를 한 번에 통째로 처리하지 않고 지정한 청크 사이즈(Chunk Size)만큼 끊어서 커밋한다.
  • 이 방식을 사용하면 트랜잭션 유지 시간이 짧아진다. 100만 건을 처리하더라도 1000건마다 DB 락을 반납하므로 중간중간 운영 트랜잭션이 수행될 틈을 준다. 단, 청크 사이즈가 너무 작으면 I/O 비용 증가로 전체 속도가 느려지고, 너무 크면 락 점유 시간이 길어지므로 보통 100에서 1000 사이에서 적절한 튜닝이 필요하다.
// 예: 1000건씩 끊어서 커밋
return stepBuilderFactory.get("step1")
    .<Order, Order>chunk(1000) // 1000건 처리 후 Commit -> Lock 해제 -> 다시 1000건 시작
    .reader(reader())
    .processor(processor())
    .writer(writer())
    .build();
  • 전략 B: Reader와 Writer의 DB 분리 단순 조회(Reader) 시간이 오래 걸려 문제가 된다면 읽기 전용 DB를 활용한다. Reader는 Slave(Replica) DB에서 읽어 락 발생과 운영 DB 부하를 줄이고, Writer는 Master DB에 필요한 순간에만 짧게 쓰기 작업을 수행한다. 다만 Slave DB의 복제 지연(Replication Lag) 가능성은 고려해야 한다.
  • 전략 C: 쿼리 튜닝 및 인덱스 사용 Update나 Delete 수행 시 Where 절에 인덱스가 걸려있지 않으면 DB는 테이블 전체 락(Table Lock)을 걸 수도 있다. 이는 서비스 장애로 이어질 수 있으므로, 배치가 수행하는 쿼리의 조건 컬럼이 반드시 인덱스를 타는지 확인해야 한다.