서버 레벨 락 vs DB 레벨 락 비교

코-드 텐카이·2025년 1월 20일

Spring Boot

목록 보기
8/10

1. 락의 범위와 목적

서버 레벨 락

  • 메모리 상의 객체를 보호
  • 하나의 JVM 내에서만 유효
  • 주로 공유 자원에 대한 동시 접근을 제어
  • 빠른 처리 속도 (메모리 레벨)
// 서버 레벨 락의 예시
public class CacheService {
    private final Object lock = new Object();
    private Map<String, Object> cache = new HashMap<>();

    public void updateCache(String key, Object value) {
        synchronized(lock) {
            // 캐시 업데이트 작업
            cache.put(key, value);
        }
    }
}

DB 레벨 락

  • 영속성 데이터를 보호
  • 모든 서버 인스턴스에 걸쳐 유효
  • 주로 데이터 정합성을 보장
  • 상대적으로 느린 처리 속도 (디스크 I/O 발생)
-- DB 레벨 락의 예시
SELECT * FROM users WHERE id = 1 FOR UPDATE;

2. 실제 사용 사례 비교

서버 레벨 락이 필요한 경우

  1. In-memory 캐시 업데이트
@Service
public class ProductCacheService {
    private final ReentrantLock lock = new ReentrantLock();
    private Map<Long, Product> productCache = new ConcurrentHashMap<>();

    public Product getProduct(Long id) {
        try {
            lock.lock();
            return productCache.computeIfAbsent(id, this::loadFromDB);
        } finally {
            lock.unlock();
        }
    }
}
  1. 단일 서버 내 카운터
@Service
public class RequestLimiter {
    private final Object lock = new Object();
    private int requestCount = 0;

    public boolean canProcess() {
        synchronized(lock) {
            if (requestCount < 100) {
                requestCount++;
                return true;
            }
            return false;
        }
    }
}

DB 레벨 락이 필요한 경우

  1. 재고 관리
@Repository
public interface StockRepository extends JpaRepository<Stock, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select s from Stock s where s.id = :id")
    Stock findByIdWithPessimisticLock(@Param("id") Long id);
}
  1. 주문 처리
@Transactional
public void processOrder(Long orderId) {
    // DB 락을 통한 동시 주문 방지
    Order order = orderRepository.findByIdWithPessimisticLock(orderId);
    // 주문 처리 로직
}

3. 주요 차이점 정리

서버 레벨 락 vs DB 레벨 락 비교

특성서버 레벨 락DB 레벨 락
유효 범위단일 JVM 내에서만 유효모든 서버/애플리케이션에서 유효
보호 대상메모리 상의 객체데이터베이스의 데이터
처리 속도빠름 (메모리 접근)상대적으로 느림 (디스크 I/O 발생)
구현 방식synchronized, ReentrantLock 등SELECT FOR UPDATE, 트랜잭션 격리 수준 등
데이터 지속성서버 재시작시 초기화영구 보존
분산 환경분산 환경에서 사용 불가분산 환경에서 사용 가능
사용 사례- 메모리 캐시 관리
- 애플리케이션 내부 카운터
- 임시 데이터 처리
- 재고 관리
- 주문/결제 처리
- 데이터 정합성이 중요한 처리
장점- 빠른 처리 속도
- 구현 간단
- 오버헤드 적음
- 강력한 데이터 정합성
- 분산 환경 지원
- 영속성 보장
단점- 단일 서버로 제한
- 서버 장애시 락 손실
- 분산 환경에서 사용 불가
- 상대적으로 느린 속도
- 데드락 가능성
- 리소스 사용량 많음

4. 실제 구현시 고려사항

서버 레벨 락 구현시

public class BetterLockExample {
    // 1. 락의 범위를 최소화
    private final ReentrantLock lock = new ReentrantLock();

    public void processWithLock() {
        // 2. try-finally로 락 해제 보장
        lock.lock();
        try {
            // 3. 크리티컬 섹션을 최소화
            doSomething();
        } finally {
            lock.unlock();
        }
    }
}

DB 레벨 락 구현시

@Service
public class OrderService {
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void processOrder(Long orderId) {
        // 1. 적절한 트랜잭션 격리 수준 선택
        // 2. 데드락 방지를 위한 타임아웃 설정
        // 3. 락의 범위를 최소화
        orderRepository.findByIdWithLock(orderId);
    }
}

5. 선택 가이드

  1. 서버 레벨 락 사용

    • 메모리 내 공유 자원 보호가 필요할 때
    • 빠른 처리가 필요할 때
    • 단일 서버에서 충분할 때
  2. DB 레벨 락 사용

    • 데이터 정합성이 중요할 때
    • 여러 서버가 동일 데이터 접근할 때
    • 트랜잭션 ACID 보장이 필요할 때

서버 레벨 락과 DB 레벨 락은 각각의 장단점과 적합한 사용 사례가 있습니다.
실제 구현시에는 두 가지를 적절히 조합하여 사용하는 것이 좋습니다.
예를 들어, 캐시와 DB를 함께 사용하는 경우 서버 레벨 락으로 캐시를 보호하고,
DB 레벨 락으로 실제 데이터를 보호하는 방식을 사용할 수 있습니다.

락(Lock) 관련 시스템 설계 Q&A

1. 다중 서버 환경에서의 동시성 제어

Q: 서버가 3대 있고, 재고 시스템을 운영 중입니다. 어떻게 동시성을 제어하시겠습니까?

1) 기본 흐름과 문제점

재고 시스템의 기본 흐름:

[주문 요청] → [재고 확인] → [재고 차감] → [주문 완료]

동시 요청 시 문제점 예시:

서버 A: 재고 100개 → 읽기 → 차감(-10) → 저장(90)
서버 B: 재고 100개 → 읽기 → 차감(-20) → 저장(80)
서버 C: 재고 100개 → 읽기 → 차감(-30) → 저장(70)
  • 실제로는 40개가 남아야 하는데 70개로 잘못 기록
  • 데이터 정합성 깨짐

2) 해결 방안

a. DB 락 사용
[서버 A] → [SELECT FOR UPDATE] → [DB]
[서버 B] → [대기] → [DB]
[서버 C] → [대기] → [DB]
  • 장점: 구현 간단, 확실한 정합성
  • 단점: 성능 저하 (대기 시간 발생)
b. 분산 락 추가
[서버들] → [Redis 분산 락] → [DB 처리]
  • Redis의 SETNX로 락 관리
  • DB 부하 감소, 더 빠른 락 처리
  • 긴 트랜잭션에 적합
c. 캐시 전략
읽기: [서버] → [캐시 조회] → [없으면 DB 조회]
쓰기: [서버] → [DB 갱신] → [캐시 무효화]
  • Cache-Aside 패턴 사용
  • 읽기 성능 향상
  • 캐시 무효화 시점 관리 필요

2. 성능과 데이터 정합성의 트레이드오프

Q: 쇼핑몰에서 상품 조회수를 카운팅해야 합니다. 어떻게 구현하시겠습니까?

1) 접근 방식 비교

a. 실시간 DB 업데이트
[조회 요청] → [DB 카운트 증가] → [응답]
  • 장점: 실시간 정확성
  • 단점: DB 부하 큼
b. 메모리 캐시 + 배치 업데이트
[조회 요청] → [Redis 카운트 증가]
   └─── 주기적으로 ──→ [DB 일괄 업데이트]
  • 장점: 성능 우수
  • 단점: 일시적 불일치

2) 구현 전략

// Redis에 카운트 증가
public void incrementViewCount(Long productId) {
    String key = "product:view:" + productId;
    redisTemplate.opsForValue().increment(key);
}

// 배치로 DB 업데이트 (스케줄링)
@Scheduled(fixedRate = 300000) // 5분마다
public void syncViewCountToDb() {
    // Redis → DB 동기화
}

3. 락의 범위와 세분성

Q: 주문 시스템에서 재고 확인부터 결제까지 어떻게 트랜잭션을 관리하시겠습니까?

1) 트랜잭션 범위 설계

[재고 확인] → [결제 요청] → [결제 완료] → [재고 차감]

2) 문제점과 해결 방안

a. 긴 트랜잭션의 위험성
락 점유 시간 = 재고 확인 + 결제 처리 시간(외부 API) + 재고 차감
  • 외부 결제 API 지연 시 락 장기 점유
  • 다른 트랜잭션 대기 증가
b. 세분화된 락 전략
[재고 확인(낙관적 락)] → [결제 요청] → [재고 차감(비관적 락)]
  • 재고 확인: 낙관적 락 (버전 관리)
  • 결제 처리: 락 없이 처리
  • 재고 차감: 비관적 락 (최소 범위)

4. 장애 상황에서의 락 처리

Q: 분산 락을 사용하는 도중 네트워크 장애가 발생하면 어떻게 처리하시겠습니까?

1) 주요 위험 상황

[서버] → [Redis 락 획득] → [네트워크 장애] → [락 해제 불가]

2) 해결 전략

a. 타임아웃 설정
// Redis 분산 락 획득 시도
boolean locked = redissonClient.tryLock("lockKey", 
    5,  // 대기 시간
    10, // 락 유효 시간
    TimeUnit.SECONDS);
b. 자동 해제 메커니즘
[락 획득] → [TTL 설정] → [처리 완료 or TTL 만료 시 자동 해제]
c. Watch Dog 패턴
[락 획득] → [백그라운드 락 갱신] → [처리 완료 시 락 해제]

Watch Dog 패턴의 기본 원리

1. 최초 락 획득
[서버] → [Redis에 락 생성 (TTL: 30초)]

2. 백그라운드 갱신
[Watch Dog Thread] → [매 10초마다 TTL 갱신]

3. 정상 케이스
락 획득 → 작업 수행 → 명시적 락 해제

4. 비정상 케이스 (서버 다운)
락 획득 → 서버 다운 → Watch Dog 중단 → TTL 만료 → 자동 락 해제

Watch Dog가 필요한 이유

긴 작업 시간

  • 락의 TTL을 길게 설정하면 → 장애 시 락이 오래 남음
  • 락의 TTL을 짧게 설정하면 → 정상 작업 중 락이 해제될 수 있음
  • Watch Dog로 → 작업 중에는 계속 갱신, 장애 시에는 빠르게 해제

장애 상황 대응

정상 상황: 락 획득 → Watch Dog 갱신 → 작업 완료 → 락 해제
장애 상황: 락 획득 → 서버 다운 → Watch Dog 중단 → TTL 만료

5. 시스템 확장성 고려

Q: 초당 100건의 주문을 1000건으로 확장해야 한다면?

초당 100건에서 1000건으로 확장하는 문제의 핵심은 "동시성 처리 방식의 변화"입니다. 원리적으로 설명해보겠습니다.

1. 병목 지점 이해

현재 시스템의 가장 큰 병목은 데이터베이스 락입니다.
왜냐하면 기본적인 DB 락 방식은 이런 흐름을 가집니다:

[요청] → [DB 락 획득 대기] → [락 획득] → [데이터 처리] → [락 해제]

이때 초당 1000건의 요청이 들어오면:

  • 각 요청이 50ms만 락을 잡고 있어도
  • 동시에 50건의 요청이 대기하게 됩니다
  • 락 경합이 심해져 전체 성능이 급격히 저하됩니다

2. 해결 방향

해결의 핵심은 "락의 범위를 줄이고, 락 획득 시간을 최소화" 하는 것입니다.

  1. 낙관적 락 전환
기존: SELECT FOR UPDATE (비관적 락)
변경: VERSION 컬럼 사용 (낙관적 락)
  • 원리: 락을 미리 잡지 않고, 변경 시점에 충돌 여부만 확인
  • 장점: 락 대기 시간이 없어짐
  • 단점: 충돌 시 재시도 필요
  1. 큐를 통한 직렬화
[요청들] → [메시지 큐] → [처리기]
  • 원리: 동시 요청을 큐에 넣고 순차적으로 처리
  • 장점: 락 경합 없이 순차 처리 가능
  • 단점: 약간의 지연 시간 발생
  1. 캐시 계층 추가
[요청] → [캐시 확인] → [DB 접근]
  • 원리: 읽기 작업을 최대한 캐시로 처리
  • 효과: DB 부하 감소, 응답 시간 개선

이렇게 하면 DB 락에 의한 병목을 피하면서도 데이터 정합성을 유지할 수 있습니다. 결국 확장성은 "얼마나 락을 효율적으로 관리하는가"에 달려있습니다.

0개의 댓글