데이터베이스 락과 동시성 제어 완벽 가이드: 공유락·배타락, 낙관적 락·비관적 락까지

데이터베이스 락과 동시성 제어 완벽 가이드: 공유락·배타락, 낙관적 락·비관적 락까지

“동시에 두 사용자가 같은 상품을 주문하면 어떻게 되나요?”, “낙관적 락과 비관적 락의 차이는?” — 동시성 제어는 백엔드 면접의 핵심 주제다. 재고 차감, 좌석 예약, 포인트 사용 등 동시 접근이 발생하는 모든 곳에서 락이 필요하다.


1. 동시성 문제가 왜 발생하는가

1.1 Lost Update (갱신 분실)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────────────────┐
│                    Lost Update 문제                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  상품 재고: 10개                                                     │
│                                                                     │
│  시간  │  Transaction A (유저1)   │  Transaction B (유저2)          │
│  ──────┼─────────────────────────┼──────────────────────────────── │
│  T1    │ 재고 읽기: 10           │                                  │
│  T2    │                         │ 재고 읽기: 10                    │
│  T3    │ 재고 = 10 - 1 = 9      │                                  │
│  T4    │ UPDATE stock = 9        │                                  │
│  T5    │                         │ 재고 = 10 - 1 = 9               │
│  T6    │                         │ UPDATE stock = 9  ← A의 결과 덮어씀!│
│                                                                     │
│  기대값: 8 (10 - 1 - 1)                                            │
│  실제값: 9 (B가 A의 업데이트를 덮어씀)                              │
│  → 1개의 주문이 사라짐!                                             │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

1.2 그 외 동시성 문제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────────────────────────────────────┐
│                    주요 동시성 문제                                   │
├──────────────────────┬──────────────────────────────────────────────┤
│ 문제                 │ 설명                                         │
├──────────────────────┼──────────────────────────────────────────────┤
│ Dirty Read           │ 커밋되지 않은 데이터를 읽음                  │
│                      │ → 롤백되면 없는 데이터를 읽은 셈            │
├──────────────────────┼──────────────────────────────────────────────┤
│ Non-Repeatable Read  │ 같은 쿼리를 두 번 실행했는데 결과가 다름    │
│                      │ → 다른 트랜잭션이 중간에 UPDATE/DELETE       │
├──────────────────────┼──────────────────────────────────────────────┤
│ Phantom Read         │ 같은 조건으로 조회했는데 행 수가 다름       │
│                      │ → 다른 트랜잭션이 중간에 INSERT             │
├──────────────────────┼──────────────────────────────────────────────┤
│ Lost Update          │ 두 트랜잭션이 같은 데이터를 동시에 수정     │
│                      │ → 먼저 쓴 값이 나중 값에 덮어씌워짐        │
└──────────────────────┴──────────────────────────────────────────────┘

2. 공유 락과 배타 락

2.1 공유 락 (Shared Lock, S-Lock)

읽기 락이라고도 한다. 여러 트랜잭션이 동시에 읽을 수 있지만, 쓰기는 막는다.

1
2
3
4
5
6
7
-- 공유 락 획득
SELECT * FROM product WHERE id = 1 LOCK IN SHARE MODE;  -- MySQL
SELECT * FROM product WHERE id = 1 FOR SHARE;           -- MySQL 8.0+

-- 공유 락이 걸린 동안:
-- 다른 트랜잭션의 SELECT → 가능 (읽기끼리는 공유)
-- 다른 트랜잭션의 UPDATE → 대기 (쓰기는 차단)

2.2 배타 락 (Exclusive Lock, X-Lock)

쓰기 락이라고도 한다. 락을 획득한 트랜잭션만 읽기/쓰기 가능하고, 나머지는 모두 차단된다.

1
2
3
4
5
6
7
-- 배타 락 획득
SELECT * FROM product WHERE id = 1 FOR UPDATE;

-- 배타 락이 걸린 동안:
-- 다른 트랜잭션의 SELECT → 가능 (일반 SELECT는 락 무관, MVCC)
-- 다른 트랜잭션의 SELECT FOR UPDATE → 대기
-- 다른 트랜잭션의 UPDATE → 대기

2.3 호환성 표

1
2
3
4
5
6
7
8
9
10
┌──────────────────────────────────────────┐
│         공유 락    배타 락               │
│ 공유 락   ✅ 호환   ❌ 충돌             │
│ 배타 락   ❌ 충돌   ❌ 충돌             │
└──────────────────────────────────────────┘

● S + S = 호환 (여러 트랜잭션이 동시에 읽기 가능)
● S + X = 충돌 (읽는 중에 쓰기 불가)
● X + X = 충돌 (쓰는 중에 다른 쓰기 불가)
● X + S = 충돌 (쓰는 중에 다른 읽기 락 불가)

3. 낙관적 락 (Optimistic Lock)

3.1 개념

“충돌이 거의 없을 것”이라고 낙관적으로 가정한다. 락을 걸지 않고 자유롭게 읽고 쓴 뒤, 커밋 시점에 충돌을 감지한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌─────────────────────────────────────────────────────────────────────┐
│                    낙관적 락 동작 원리                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  product 테이블: id=1, stock=10, version=1                         │
│                                                                     │
│  Transaction A                   Transaction B                     │
│  ─────────────                   ─────────────                     │
│  SELECT ... WHERE id=1           SELECT ... WHERE id=1             │
│  (stock=10, version=1)           (stock=10, version=1)             │
│       │                                │                            │
│  UPDATE product                        │                            │
│  SET stock=9, version=2                │                            │
│  WHERE id=1 AND version=1              │                            │
│  → 성공! (version 1→2)                │                            │
│       │                                │                            │
│       │                          UPDATE product                    │
│       │                          SET stock=9, version=2            │
│       │                          WHERE id=1 AND version=1          │
│       │                          → 실패! (version이 이미 2)        │
│       │                          → 0 rows affected                 │
│       │                          → OptimisticLockException         │
│                                                                     │
│  핵심: WHERE 절에 version을 포함하여 "내가 읽은 것과 같은 상태인지" │
│  확인한다. 다른 트랜잭션이 먼저 수정했으면 version이 달라져서 실패.  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

3.2 JPA에서의 구현 (@Version)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
public class Product {
    @Id @GeneratedValue
    private Long id;

    private String name;
    private int stock;

    @Version  // 낙관적 락 버전 필드
    private Long version;

    public void decreaseStock(int quantity) {
        if (this.stock < quantity) {
            throw new IllegalStateException("재고 부족");
        }
        this.stock -= quantity;
    }
}
1
2
// JPA가 자동으로 version을 체크하는 UPDATE 쿼리 생성
// UPDATE product SET stock=9, version=2 WHERE id=1 AND version=1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
@RequiredArgsConstructor
public class OrderService {

    private final ProductRepository productRepository;

    @Transactional
    public void order(Long productId, int quantity) {
        Product product = productRepository.findById(productId)
                .orElseThrow();

        product.decreaseStock(quantity);
        // 트랜잭션 커밋 시 version 체크
        // 충돌 시 OptimisticLockException 발생
    }
}

3.3 충돌 시 재시도

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
@RequiredArgsConstructor
public class OrderFacade {

    private final OrderService orderService;

    public void orderWithRetry(Long productId, int quantity) {
        int maxRetries = 3;
        for (int i = 0; i < maxRetries; i++) {
            try {
                orderService.order(productId, quantity);
                return; // 성공하면 종료
            } catch (OptimisticLockException e) {
                if (i == maxRetries - 1) {
                    throw new BusinessException(ErrorCode.CONFLICT,
                            "주문 처리 중 충돌이 발생했습니다. 다시 시도해주세요.");
                }
                // 잠시 대기 후 재시도
                try { Thread.sleep(50); } catch (InterruptedException ignored) {}
            }
        }
    }
}

4. 비관적 락 (Pessimistic Lock)

4.1 개념

“충돌이 자주 발생할 것”이라고 비관적으로 가정한다. 데이터를 읽는 시점에 즉시 락을 획득하여 다른 트랜잭션의 접근을 차단한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────────────────────┐
│                    비관적 락 동작 원리                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Transaction A                   Transaction B                     │
│  ─────────────                   ─────────────                     │
│  SELECT ... FOR UPDATE           SELECT ... FOR UPDATE             │
│  WHERE id=1                      WHERE id=1                        │
│  → 락 획득! (stock=10)          → 대기... (락 획득 불가)           │
│       │                                │                            │
│  UPDATE stock=9                        │ (대기 중)                  │
│  COMMIT                                │                            │
│  → 락 해제                             │                            │
│       │                          → 락 획득! (stock=9)              │
│       │                          UPDATE stock=8                    │
│       │                          COMMIT                            │
│                                                                     │
│  결과: stock=8 (정확!)                                              │
│  → A가 끝날 때까지 B가 대기하므로 Lost Update 방지                  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

4.2 JPA에서의 구현

1
2
3
4
5
6
7
public interface ProductRepository extends JpaRepository<Product, Long> {

    // 비관적 락 — SELECT ... FOR UPDATE
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Optional<Product> findByIdWithLock(@Param("id") Long id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
@RequiredArgsConstructor
public class OrderService {

    private final ProductRepository productRepository;

    @Transactional
    public void order(Long productId, int quantity) {
        // SELECT ... FOR UPDATE → 다른 트랜잭션은 이 행에 접근 시 대기
        Product product = productRepository.findByIdWithLock(productId)
                .orElseThrow();

        product.decreaseStock(quantity);
        // COMMIT 시 락 자동 해제
    }
}

4.3 락 타임아웃 설정

1
2
3
4
5
6
7
8
// 락 획득 대기 시간 제한 (무한 대기 방지)
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints({
    @QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")
})
@Query("SELECT p FROM Product p WHERE p.id = :id")
Optional<Product> findByIdWithLock(@Param("id") Long id);
// 3초 안에 락을 못 얻으면 PessimisticLockException 발생

5. 낙관적 락 vs 비관적 락 비교

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────────────────────┐
│            낙관적 락 vs 비관적 락                                    │
├──────────────────────┬──────────────────────────────────────────────┤
│   낙관적 락          │   비관적 락                                   │
├──────────────────────┼──────────────────────────────────────────────┤
│ 충돌이 드물다고 가정 │ 충돌이 잦다고 가정                           │
│ 락을 걸지 않음       │ 읽는 시점에 락 획득                          │
│ 커밋 시 충돌 감지    │ 다른 트랜잭션을 대기시킴                     │
│ @Version 필드 필요   │ SELECT ... FOR UPDATE                       │
│ 충돌 시 예외 발생    │ 충돌 시 대기                                 │
│ → 재시도 로직 필요   │ → 타임아웃 설정 필요                        │
│ 성능 좋음 (락 없음)  │ 대기로 인한 처리량 저하                     │
│ DB 락 미사용         │ DB 행 락 사용                               │
├──────────────────────┼──────────────────────────────────────────────┤
│ 적합한 상황:         │ 적합한 상황:                                  │
│ ● 읽기 많고 쓰기 적음│ ● 쓰기가 많고 충돌 잦음                     │
│ ● 충돌 확률 낮음     │ ● 충돌 시 재시도 비용이 큰 경우             │
│ ● 게시글 수정        │ ● 재고 차감, 좌석 예약                      │
│ ● 사용자 정보 수정   │ ● 포인트/잔액 차감                          │
│ ● 설정값 변경        │ ● 선착순 이벤트                             │
└──────────────────────┴──────────────────────────────────────────────┘

5.1 선택 기준

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"충돌이 자주 일어나는가?"
  ├── 거의 없음 → 낙관적 락 (@Version)
  │   예: 사용자 프로필 수정, 게시글 수정
  │
  └── 자주 일어남 → 비관적 락 (FOR UPDATE)
      예: 재고 차감, 좌석 예약, 포인트 차감

"충돌 시 재시도 비용이 큰가?"
  ├── 재시도 가능 → 낙관적 락
  └── 재시도 어려움 (결제 등) → 비관적 락

"처리량(throughput)이 중요한가?"
  ├── 중요 → 낙관적 락 (대기 없음)
  └── 정합성이 더 중요 → 비관적 락

6. 락과 트랜잭션 격리 수준의 차이

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────────────────┐
│          트랜잭션 격리 수준 vs 락                                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  트랜잭션 격리 수준 (Isolation Level):                               │
│  ● DB 전체에 적용되는 정책                                          │
│  ● "다른 트랜잭션의 변경을 어디까지 보여줄 것인가"                  │
│  ● READ COMMITTED → REPEATABLE READ → SERIALIZABLE                │
│  ● Dirty Read, Non-Repeatable Read, Phantom Read 방지              │
│                                                                     │
│  락 (Lock):                                                         │
│  ● 특정 데이터(행)에 대한 접근 제어                                 │
│  ● "이 데이터를 동시에 수정하지 못하게 막는다"                      │
│  ● 공유 락, 배타 락, 낙관적 락, 비관적 락                          │
│  ● Lost Update 방지                                                │
│                                                                     │
│  핵심 차이:                                                         │
│  격리 수준 = "읽기"에 대한 정책                                     │
│  락 = "쓰기"에 대한 제어                                            │
│                                                                     │
│  REPEATABLE READ여도 Lost Update는 방지 못함!                       │
│  → 격리 수준만으로는 부족, 락이 필요                                │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

7. 데드락

7.1 데드락이 발생하는 이유

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─────────────────────────────────────────────────────────────────────┐
│                    데드락 발생 예시                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Transaction A                   Transaction B                     │
│  ─────────────                   ─────────────                     │
│  UPDATE product SET stock=9      UPDATE order SET status='PAID'    │
│  WHERE id=1;                     WHERE id=100;                     │
│  → product(1) 락 획득            → order(100) 락 획득              │
│       │                                │                            │
│  UPDATE order SET status='DONE'  UPDATE product SET stock=8        │
│  WHERE id=100;                   WHERE id=1;                       │
│  → order(100) 락 필요            → product(1) 락 필요              │
│  → B가 가지고 있음 → 대기!      → A가 가지고 있음 → 대기!         │
│       │                                │                            │
│  서로 상대방의 락을 기다림 → 영원히 대기 → 데드락!                  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

7.2 데드락 대응

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 락 순서 통일
   → 모든 트랜잭션이 product → order 순서로 락을 획득하면
   → 순환 대기가 발생하지 않음

2. 락 타임아웃 설정
   → 일정 시간 안에 락을 못 얻으면 포기하고 재시도
   SET innodb_lock_wait_timeout = 5;  -- 5초 대기 후 타임아웃

3. 트랜잭션을 짧게 유지
   → 락을 보유하는 시간을 최소화

4. DB 자동 감지
   → MySQL InnoDB는 데드락을 자동 감지하여 한쪽을 롤백
   → "Deadlock found when trying to get lock; try restarting transaction"

8. JPA/Spring에서 락을 다루는 방법

8.1 JPA 락 모드

1
2
3
4
5
6
7
8
// 낙관적 락
@Lock(LockModeType.OPTIMISTIC)           // 조회 시 version 체크
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT) // 조회만 해도 version 증가

// 비관적 락
@Lock(LockModeType.PESSIMISTIC_READ)     // SELECT ... FOR SHARE
@Lock(LockModeType.PESSIMISTIC_WRITE)    // SELECT ... FOR UPDATE (가장 많이 사용)
@Lock(LockModeType.PESSIMISTIC_FORCE_INCREMENT) // FOR UPDATE + version 증가

8.2 Named Lock (MySQL)

1
2
3
4
5
6
// MySQL의 GET_LOCK — 테이블/행이 아닌 이름으로 락
@Query(value = "SELECT GET_LOCK(:key, 3)", nativeQuery = true)
void getLock(@Param("key") String key);

@Query(value = "SELECT RELEASE_LOCK(:key)", nativeQuery = true)
void releaseLock(@Param("key") String key);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
@RequiredArgsConstructor
public class NamedLockFacade {

    private final LockRepository lockRepository;
    private final OrderService orderService;

    @Transactional
    public void order(Long productId, int quantity) {
        try {
            lockRepository.getLock("product_" + productId);
            orderService.order(productId, quantity);
        } finally {
            lockRepository.releaseLock("product_" + productId);
        }
    }
}

8.3 분산 락 (Redis — Redisson)

여러 서버에서 동시에 접근하는 경우, DB 락으로는 부족할 수 있다. Redis 분산 락을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component
@RequiredArgsConstructor
public class RedissonLockFacade {

    private final RedissonClient redissonClient;
    private final OrderService orderService;

    public void order(Long productId, int quantity) {
        RLock lock = redissonClient.getLock("lock:product:" + productId);

        try {
            // 10초 대기, 1초 후 락 자동 해제
            boolean acquired = lock.tryLock(10, 1, TimeUnit.SECONDS);
            if (!acquired) {
                throw new BusinessException(ErrorCode.CONFLICT, "잠시 후 다시 시도해주세요");
            }
            orderService.order(productId, quantity);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BusinessException(ErrorCode.INTERNAL_ERROR);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

8.4 락 방식별 비교

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────────────────────────────────────┐
│                락 방식별 비교                                        │
├──────────────┬───────────────┬───────────────┬──────────────────────┤
│              │ 낙관적 락     │ 비관적 락     │ Redis 분산 락        │
├──────────────┼───────────────┼───────────────┼──────────────────────┤
│ 구현         │ @Version      │ FOR UPDATE    │ Redisson             │
│ 충돌 처리    │ 예외 + 재시도 │ 대기          │ 대기 + 타임아웃      │
│ 성능         │ 좋음          │ 보통          │ 좋음                 │
│ 적용 범위    │ 단일 DB       │ 단일 DB       │ 분산 환경            │
│ 적합한 상황  │ 충돌 적음     │ 충돌 많음     │ 멀티 서버            │
└──────────────┴───────────────┴───────────────┴──────────────────────┘

면접 예상 질문 & 답변

Q1. 낙관적 락과 비관적 락의 차이를 설명해주세요.

낙관적 락은 충돌이 드물다고 가정하고, 데이터를 읽을 때 락을 걸지 않습니다. 대신 @Version 필드를 사용하여 커밋 시점에 version을 비교합니다. 다른 트랜잭션이 먼저 수정했으면 version이 달라져 OptimisticLockException이 발생합니다.

비관적 락은 충돌이 잦다고 가정하고, 데이터를 읽는 시점에 SELECT ... FOR UPDATEDB 행 락을 획득합니다. 다른 트랜잭션은 락이 해제될 때까지 대기합니다.

충돌이 적으면 낙관적 락(게시글 수정), 충돌이 잦으면 비관적 락(재고 차감, 좌석 예약)을 사용합니다.

Q2. 공유 락과 배타 락의 차이는?

공유 락(S-Lock) 은 읽기 전용 락으로, 여러 트랜잭션이 동시에 공유 락을 획득하여 읽을 수 있습니다. 하지만 공유 락이 걸린 동안 쓰기(UPDATE)는 차단됩니다.

배타 락(X-Lock) 은 쓰기 락으로, 하나의 트랜잭션만 획득할 수 있습니다. 배타 락이 걸린 동안 다른 트랜잭션의 락 획득이 모두 차단됩니다.

공유 락끼리는 호환되지만, 배타 락은 다른 모든 락과 충돌합니다.

Q3. 데드락이 발생하는 이유와 대응 방법은?

데드락은 두 트랜잭션이 서로 상대방이 가진 락을 기다리면서 영원히 대기하는 상태입니다. A가 product 락을 가지고 order 락을 기다리고, B가 order 락을 가지고 product 락을 기다리면 데드락입니다.

대응 방법은: 1) 락 획득 순서를 통일하여 순환 대기를 방지, 2) 락 타임아웃을 설정하여 무한 대기 방지, 3) 트랜잭션을 짧게 유지하여 락 보유 시간 최소화입니다. MySQL InnoDB는 데드락을 자동 감지하여 한쪽 트랜잭션을 롤백합니다.

Q4. JPA에서 @Version은 어떻게 동작하나요?

@Version이 붙은 필드가 있으면, JPA가 UPDATE 시 자동으로 WHERE 절에 version 조건을 추가합니다. UPDATE product SET stock=9, version=2 WHERE id=1 AND version=1 형태입니다.

다른 트랜잭션이 먼저 수정하여 version이 증가했으면, WHERE 조건에 맞는 행이 없어 0 rows affected가 됩니다. JPA는 이를 감지하여 OptimisticLockException을 발생시킵니다.

Q5. 재고 차감에서 동시성 문제를 어떻게 해결하나요?

가장 일반적인 방법은 비관적 락입니다. @Lock(PESSIMISTIC_WRITE)로 SELECT … FOR UPDATE를 실행하면, 해당 행에 배타 락이 걸려 다른 트랜잭션은 대기합니다. 순차적으로 처리되므로 Lost Update가 방지됩니다.

충돌이 적으면 낙관적 락(@Version)으로도 가능하지만, 인기 상품의 동시 주문 같은 경우에는 충돌이 잦아 재시도가 반복되므로 비관적 락이 적합합니다.

서버가 여러 대인 분산 환경에서는 Redis 분산 락(Redisson)을 사용합니다.

Q6. 트랜잭션 격리 수준과 락의 차이는?

격리 수준은 다른 트랜잭션의 변경 사항을 읽기에서 어디까지 보여줄 것인가에 대한 정책입니다. Dirty Read, Non-Repeatable Read, Phantom Read 같은 읽기 문제를 방지합니다.

은 특정 데이터에 대한 동시 쓰기를 제어합니다. 격리 수준만으로는 Lost Update를 방지할 수 없기 때문에, 동시에 같은 데이터를 수정하는 상황에서는 별도의 락이 필요합니다.

Q7. 분산 환경에서 동시성 제어는 어떻게 하나요?

서버가 여러 대인 분산 환경에서는 DB 락만으로는 부족할 수 있습니다. Redis 기반 분산 락을 사용합니다. Redisson 라이브러리의 RLock이 대표적이며, tryLock(대기시간, 자동해제시간)으로 락을 획득합니다.

원리는 Redis에 특정 key를 SET하여 락을 표현하고, 락을 가진 쪽만 작업을 수행합니다. 서버가 죽어도 TTL로 락이 자동 해제되어 교착 상태를 방지합니다.


마무리

동시성 제어의 핵심을 정리하면:

  • Lost Update: 두 트랜잭션이 같은 데이터를 동시 수정 → 먼저 쓴 값 유실
  • 공유 락 / 배타 락: DB 수준의 행 락, 읽기/쓰기 제어
  • 낙관적 락: @Version으로 커밋 시 충돌 감지, 충돌 적을 때 사용
  • 비관적 락: FOR UPDATE로 즉시 락 획득, 충돌 잦을 때 사용
  • 데드락: 락 순서 통일, 타임아웃, 짧은 트랜잭션으로 방지
  • 분산 환경: Redis 분산 락 (Redisson)

면접에서는 “어떤 상황에 어떤 락을 선택하는가”를 물어본다. 상황별 트레이드오프를 이해하고 설명할 수 있으면 충분하다.