Java
DB의 동시성(Concurrency) 제어와 트랜잭션
haleylog
2025. 4. 16. 08:00
DB의 동시성(Concurrency) 제어와 트랜잭션 설계 고민
서비스를 운영하다 보면 "이 기능은 하나의 트랜잭션으로 묶여야 해!" 하는 순간이 온다.
트랜잭션 처리가 필요한 상황과 발생 할 수 있는 이슈에 대해 정리하고,
이런 이슈를 방지할 방안에 대해 고민해보려고 한다.
이슈1: 동시에 같은 요청이 들어오면? (A.K.A. 따닥)
다수 혹은 한명의 사용자가 동시에 같은 API를 여러번 호출해서 특정 리소스A를 수정이나 삭제하려고 한다면?
-> 예상치 못한 중복 처리, 데이터 불일치가 발생할 수 있음 (전형적인 동시성 문제)
728x90
해결방안1: 비관적 락 (Pessimistic Lock)
row-level lock 직접 걸기
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Data> findByIdForUpdate(Long id);
- 장점: 안전하게 처리 가능
- 단점: 성능 저하, 데드락
해결방안2: 낙관적 락 (Optimistic Lock)
version 필드를 두고 충돌이 날 것 같지 않다고 가정하고 먼저 처리한 뒤
최종 커밋 시점에 version 비교하여 충돌 여부 확인
@Entity
public class Product {
@Id
private Long id;
@Version
private Long version;
private int stock;
}
- 장점: 락을 걸지 않아서 성능이 좋음
- 단점: 충돌 발생 시 예외 처리 및 재시도 로직 필요
동시 수정이 많을수록 실패율이 높아짐
해결방안3: 분산 락 (Distributed Lock)
로직 단에서 lock을 걸 때, 다중화 환경 지원을 위해 Redis 기반 락(Redisson) 등을 활용하여 직접 특정 key에 락 걸고 해제
RLock lock = redissonClient.getLock("lock:data1");
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
updateData();
}
} finally {
lock.unlock();
}
- 장점: 여러 서버에서 동시에 접근할 때도 안전
트래픽 높은 분산 환경에서 유용 - 단점: Redis가 단일 실패 지점(SPOF)이 될 수 있음
락 유실, 타임아웃 등 관리 필요
해결방안4: 요청 자체의 중복 방지 (Idempotent 처리)
각 요청에 유일한 ID (X-Request-Id) 부여 및 체크하여 중복 방지
@PostMapping("/api/process")
public ResponseEntity<?> process(@RequestHeader("X-Request-Id") String requestId) {
if (requestService.isAlreadyProcessed(requestId)) {
return ResponseEntity.status(409).body("이미 처리된 요청입니다");
}
requestService.process();
}
- 장점: 중복 호출에 DB Lock 없이 안전하게 동작
- 단점: 중복 체크용 저장소 필요 (DB, Redis 등)
DB와는 관계 없이 요청에 대한 중복만 체크 가능
이슈2: 트랜잭션 안에 오래 걸리는 작업이 있을때
예를 들어, 아래와 같은 기능의 메서드를 호출하는 API 를 만든다.
@Transactional
public void process() {
deleteData(); // 1. 데이터 삭제
someLongTask(); // 2. 오래 걸리는 작업 (ex. 외부 API, 복잡한 연산 등)
insertData(); // 3. 새로운 데이터 추가
}
이런 트랜잭션이 걸린 메서드가 있을 때, 일반적으로 잘 동작하지만
트래픽이 많아지거나 동시에 여러 요청이 들어오면 아래와 같은 문제가 발생한다.
- DB 리소스를 오래 점유 → Lock이 유지됨
- 다른 요청들이 해당 리소스에 접근 시 지연(Lock 대기) 또는 타임아웃 발생
- 결국 전체 시스템의 응답 속도와 안정성에 영향을 줌
해결방안1: 트랜잭션 분리 전략
트랜잭션을 분리하여 오래 걸리는 작업은 비동기 처리(Kafka 등)
@Transactional
public void deleteData() { // 삭제는 즉시 처리
repository.deleteById(id);
}
public void enqueueProcessingJob() { // 오래걸리는 작업은 비동기 메시지 큐로 처리
kafkaProducer.send("data2-create", payload);
}
- 장점: 트랜잭션 짧아져서 안정성이 높고 속도와 처리량 개선 가능
- 단점: 분산 시스템 복잡도 증가. 재시도, 보상 로직 추가 필요
728x90
반응형