CS

락(Lock)과 동시성

baek-dev 2025. 2. 18. 18:49

멀티스레드 환경에서 데이터 일관성을 유지하고 동시성을 제어하기 위해 **락(Lock)**을 사용함.

락을 적용하는 방식에는 크게 **비관적 락(Pessimistic Lock)**과 **낙관적 락(Optimistic Lock)**이 있음.

 

📌 1. 동시성(Concurrency) 문제란?

여러 개의 스레드(또는 프로세스)가 같은 데이터를 동시에 접근할 때, 데이터 일관성이 깨지는 문제를 의미함.

예를 들어, 은행 계좌에서 잔액을 조회하고 동시에 출금하는 경우를 생각해보자.

 

예제 (동시성 문제)

1. 스레드 A: 계좌 잔액 조회 → 100,000원

2. 스레드 B: 계좌 잔액 조회 → 100,000원

3. 스레드 A: 50,000원 출금 → 50,000원 남음

4. 스레드 B: 30,000원 출금 → 70,000원 남음 ❌ (데이터 불일치)

 

위 상황에서 올바른 결과는 잔액 20,000원이 되어야 하지만, 데이터가 잘못 처리됨.

이를 해결하려면 동시성을 다루는 기법(락, 트랜잭션 등)을 사용해야 함.

 

📌 2. 비관적 락 (Pessimistic Lock)

✅ 비관적 락이란?

 

“다른 트랜잭션이 데이터를 수정할 가능성이 높다고 가정하고, 먼저 락을 걸어서 충돌을 방지하는 방식”

 

즉, 데이터를 읽을 때(lock을 걸어) 다른 트랜잭션이 접근하지 못하도록 차단하는 방법임.

 

🔹 비관적 락 적용 방법

Shared Lock (읽기 잠금, S-Lock): 데이터를 읽는 동안 다른 트랜잭션이 쓰기 작업을 하지 못하도록 차단함.

Exclusive Lock (쓰기 잠금, X-Lock): 데이터를 수정하는 동안 다른 트랜잭션이 읽기와 쓰기를 모두 못 하도록 차단함.

 

✅ 비관적 락 예제 (Java - JPA)

 

비관적 락을 사용하면, 특정 데이터를 가져올 때 락을 걸어서 다른 트랜잭션이 접근하지 못하도록 함.

@Entity
public class Account {
    @Id @GeneratedValue
    private Long id;
    private int balance;
}

public interface AccountRepository extends JpaRepository<Account, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT a FROM Account a WHERE a.id = :id")
    Account findByIdWithLock(@Param("id") Long id);
}

 

🔹 설명

@Lock(LockModeType.PESSIMISTIC_WRITE):

**쓰기 락(Exclusive Lock)**을 걸어서 다른 트랜잭션이 동시에 접근하지 못하도록 차단함.

findByIdWithLock(1L)을 호출하면, 해당 계좌 정보를 가져오는 동안 다른 트랜잭션이 접근 불가함.

 

✅ 비관적 락의 장점 & 단점

장점 단점
데이터 정합성이 보장됨 성능이 낮아질 수 있음 (병목 현상 발생)
동시성 충돌을 사전에 방지 락을 오래 유지하면 데드락(Deadlock) 가능성 증가

 

📌 3. 낙관적 락 (Optimistic Lock)

✅ 낙관적 락이란?

 

“다른 트랜잭션이 데이터를 수정할 가능성이 낮다고 가정하고, 락을 사용하지 않되, 데이터 충돌이 발생하면 감지하는 방식”

 

즉, 트랜잭션이 데이터를 수정할 때 버전 정보를 확인하여 충돌을 감지하고 처리함.

 

✅ 낙관적 락 예제 (Java - JPA)

 

JPA에서는 @Version을 사용하여 낙관적 락을 쉽게 적용할 수 있음.

@Entity
public class Account {
    @Id @GeneratedValue
    private Long id;
    private int balance;

    @Version
    private Integer version;
}

 

🔹 낙관적 락 동작 방식

1. 데이터를 조회하면, version 값도 함께 가져옴.

2. 데이터를 수정할 때, 현재 version과 저장된 version을 비교

일치하면 업데이트 수행 → version 값 증가

일치하지 않으면 예외(OptimisticLockException) 발생

 

JPA에서 낙관적 락을 적용한 메서드

@Transactional
public void updateBalance(Long id, int amount) {
    Account account = accountRepository.findById(id).orElseThrow();
    account.setBalance(account.getBalance() + amount);
}

같은 데이터를 여러 트랜잭션이 동시에 수정하려 하면 충돌이 감지됨.

OptimisticLockException이 발생하면 재시도 로직을 적용하여 해결할 수 있음.

 

✅ 낙관적 락의 장점 & 단점

장점 단점
락을 걸지 않아 성능이 좋음 충돌 발생 시 트랜잭션 재시도가 필요함
병목 현상이 적음 동시성 충돌이 자주 발생하면 오히려 성능이 낮아짐

 

📌 4. 비관적 락 vs 낙관적 락 비교

비교 항목 비관적 락 (Pessimistic Lock) 낙관적 락 (Optimistic Lock)
동작 방식 데이터 접근 시 락을 걸어 다른 트랜잭션 차단 데이터 수정 시 버전 정보를 비교하여 충돌 감지
충돌 가능성 충돌이 자주 발생하는 경우 적합 충돌이 적은 경우 적합
성능 낮음 (동시성 제어 비용 발생) 높음 (락 없이 동작)
트랜잭션 재시도 필요 없음 필요할 수 있음
사용 예시 은행 계좌, 재고 관리 (데이터 일관성이 중요한 경우) 게시판 댓글 수정, 비밀번호 변경 (충돌 가능성이 낮은 경우)

 

언제 비관적 락을 쓰고, 언제 낙관적 락을 써야 할까?

비관적 락데이터 정합성이 최우선인 경우 (예: 은행 거래, 재고 관리)

낙관적 락충돌이 드물고 성능이 중요한 경우 (예: 쇼핑몰 상품 조회, 댓글 수정)

 

📌 5. 동시성 문제 해결을 위한 추가 방법

✅ (1) Synchronized (Java)

 

멀티스레드 환경에서 특정 코드 블록에 락을 걸어 한 번에 하나의 스레드만 접근하도록 보장함.

public class BankAccount {
    private int balance = 100;

    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            balance -= amount;
        }
    }
}

 

단점: 성능 저하(스레드가 락을 기다려야 함)

 

✅ (2) 데이터베이스 트랜잭션 + Serializable Isolation Level

트랜잭션의 격리 수준(Isolation Level)을 조정하여 동시성 문제 해결 가능

SERIALIZABLE 수준을 사용하면 동시에 하나의 트랜잭션만 접근 가능하지만, 성능 저하가 발생할 수 있음.

 

✅ (3) Redis 분산 락 활용

분산 환경에서 락을 관리하기 위해 Redis를 활용할 수도 있음.

SETNX (SET if Not Exists) 명령을 사용하여 락을 획득하고, 타임아웃을 설정하여 데드락 방지 가능.

 

🚀 결론

동시성 문제를 해결하기 위해 **비관적 락(Pessimistic Lock)**과 **낙관적 락(Optimistic Lock)**을 사용할 수 있음.

비관적 락정확한 데이터 보장이 필요할 때, 낙관적 락충돌 가능성이 낮은 경우에 적합함.

추가적으로 트랜잭션 격리 수준, Redis 분산 락, synchronized 블록 등을 활용하여 동시성을 제어할 수도 있음.

 

 

 

 

출처 : ChatGPT

'CS' 카테고리의 다른 글

트러블 슈팅  (0) 2025.02.28
SSE (Server-Sent Events)  (0) 2025.02.19
OCP 원칙 (Open-Closed Principle)  (0) 2025.02.17
다형성  (1) 2025.02.16
DFS, BFS  (0) 2025.02.14