동시에 포인트를 충전하거나 사용하는 상황에서 동시성 제어를 적절히 하지 않으면 다음과 같은 문제가 발생할 수 있습니다.
- 데이터 불일치: 동시에 여러 작업이 수행될 때, 데이터베이스의 상태가 일관되지 않을 수 있습니다.
- 정합성 문제: 포인트 충전 또는 사용 내역이 올바르게 기록되지 않거나, 포인트 잔액이 잘못 계산될 수 있습니다.
- 포인트 잔액 초과 사용: 동시에 포인트를 사용할 경우, 포인트 잔액이 충분하지 않음에도 불구하고 포인트가 중복으로 사용될 수 있습니다.
이를 방지하기 위해 다음과 같은 방식으로 동시성 제어를 하였습니다.
본 프로젝트에서는 ReentrantLock을 사용하여 동시성을 제어하였습니다.
- 사용 이유: 자바의
synchronized
키워드보다 유연하게 동작할 수 있으며, 명시적으로 잠금과 해제를 제어할 수 있습니다. - 동작 원리: 특정 사용자가 포인트를 충전하거나 사용할 때,
ReentrantLock
을 통해 해당 작업을 단일 스레드에서만 처리할 수 있도록 하여 동시에 여러 스레드가 동일한 자원(포인트 잔고)에 접근하는 것을 방지합니다. - 사용 예시
private final Lock lock = new ReentrantLock(); public UserPoint transaction(long id, long amount, TransactionType type) { lock.lock(); try { // 트랜잭션 로직 } finally { lock.unlock(); } }
특성 | ReentrantLock |
synchronized |
---|---|---|
락 획득과 해제 | 명시적(lock()/unlock()) - 시작과 끝을 명시하기 때문에 여러 메서드에 나눠서 작성 가능 |
암시적(블록이 끝날 때 자동 해제) - 메서드 안에 임계 영역의 시작과 끝이 존재 |
공정성 설정 | Fair 모드를 사용하여 공정성 보장 가능 - 공정한 lock을 사용할 경우 경쟁이 발생했을 때 가장 오랫동안 기다린 쓰레드에게 lock을 제공 |
불가능 |
락의 상태 확인 | isLocked() , isHeldByCurrentThread() 등을 통해 상태 확인 가능 |
불가능 |
조건 변수 | 여러 Condition 객체를 사용할 수 있음. |
단일 조건 변수(암시적) |
성능 | 높은 수준의 제어로 더 높은 성능 제공 가능 (명시적 제어) | 간단한 동기화에 더 적합 (암시적 제어) |
ReentrantLock
은synchronized
와 비교하여 다음과 같은 장점을 가집니다:- 공정성(Fairness) 설정:
ReentrantLock
은 공정성 설정이 가능하여, 대기 중인 스레드가 락을 공정하게 획득할 수 있습니다. - 더 많은 제어:
ReentrantLock
은lock()
과unlock()
메서드를 사용해 락을 명시적으로 제어할 수 있어, 복잡한 락 해제 로직을 구현할 수 있습니다. - 조건 변수(Condition Variables):
ReentrantLock
은Condition
객체를 통해 세밀한 조건 동기화를 지원합니다.
- 공정성(Fairness) 설정:
- 단, 간단한 동기화의 경우
synchronized
키워드가 더 간결하고 이해하기 쉬운 장점을 가지고 있으므로, 상황에 따라 적절한 방식을 선택해야 합니다.
- 사용 이유: 비동기적으로 여러 스레드가 동시에 작업을 요청하는 시나리오를 테스트하기 위해 사용합니다.
- 동작 원리: 여러 개의
CompletableFuture
작업을 생성하고,allOf()
메서드를 통해 모든 작업이 동시에 실행되도록 합니다. 이렇게 함으로써 다수의 사용자 요청이 동시에 들어오는 상황을 시뮬레이션할 수 있습니다. - 사용 예시
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> { pointService.transaction(user1Id, 1000, TransactionType.CHARGE); }); CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> { pointService.transaction(user2Id, 1000, TransactionType.USE); }); CompletableFuture.allOf(future1, future2).join();
동시성 제어가 제대로 동작하는지 검증하기 위해 다음과 같은 테스트를 수행하였습니다:
- 동시 포인트 충전 테스트: 사용자가 동시에 포인트를 충전할 때, 총 포인트가 정확하게 증가하는지 확인하였습니다.
- 동시 포인트 사용 테스트: 사용자가 동시에 포인트를 사용할 때, 포인트 부족 시 예외가 발생하는지 확인하였습니다.
- 동시 포인트 충전/사용 혼합 테스트: 사용자가 동시에 포인트 충전과 사용을 혼합하여 요청할 때, 순서대로 처리되는지 확인하였습니다.
- 동시에 포인트 충전 요청 한도 초과로 실패: 포인트 충전시 포인트 한도가 초과될 경우, 한도 이내에서 충전이 완료 되는지 확인하였습니다.
- 동시에 포인트 사용 요청시 잔액 부족으로 실패: 포인트 사용시 잔액이 부족한 경우, 사용되지 않는지 확인하였습니다.
ReentrantLock
을 통해 동시성 제어를 구현하였으며, 테스트를 통해 이를 검증하였습니다. synchronized
보다 공정성 있고, 명시적으로 세밀한 제어가 가능하다는 것을 알 수 있었습니다. 테스트 코드를 통하여 정합성 문제, 정책(잔고부족, 최대잔고) 문제 등에 대해 문제가 발생하지 않는지 확인할 수 있었습니다.