동시성 제어 접근 방법
1. Database Unique Index
개념
- 데이터베이스 테이블의 특정 컬럼 또는 컬럼 조합에 대해 유일성을 보장하는 인덱스입니다.
- 중복된 값이 입력되는 것을 방지합니다.
장점
- 데이터 무결성을 데이터베이스 레벨에서 보장합니다.
- 검색 성능을 향상시킵니다.
- 동시성 문제를 방지할 수 있습니다 (예: 중복 예약 방지).
단점
- 인덱스 유지에 따른 약간의 성능 오버헤드가 발생할 수 있습니다.
- 대량의 데이터 입력 시 성능 저하가 발생할 수 있습니다.
2. Database Lock
a) 비관적 락 (Pessimistic Lock)
개념
- 데이터를 읽는 시점에 락을 걸어 다른 트랜잭션의 접근을 차단합니다.
장점
- 동시성 문제를 확실하게 방지할 수 있습니다.
- 데이터 일관성을 강력하게 보장합니다.
단점
- 동시 처리 성능이 떨어질 수 있습니다.
- 데드락 발생 가능성이 있습니다.
b) 낙관적 락 (Optimistic Lock)
개념
- 데이터 수정 시 충돌 여부를 확인하고, 충돌 시 재시도하는 방식입니다.
- 주로 버전 필드를 사용하여 구현합니다.
장점
- 동시성이 높은 환경에서 성능이 좋습니다.
- 데드락 위험이 없습니다.
단점
- 충돌 시 재시도 로직이 필요합니다.
- 빈번한 충돌 시 성능 저하가 발생할 수 있습니다.
3. Distributed Lock
개념
- 분산 시스템에서 여러 노드 간의 동기화를 위해 사용되는 락 메커니즘입니다.
- 주로 Redis, ZooKeeper 등을 사용하여 구현합니다.
장점
- 분산 환경에서 동시성 제어가 가능합니다.
- 단일 장애점(SPOF)을 피할 수 있습니다.
- 확장성이 좋습니다.
단점
- 구현이 복잡할 수 있습니다.
- 네트워크 지연이나 장애에 영향을 받을 수 있습니다.
- 락 획득/해제 과정에서 오버헤드가 발생할 수 있습니다.
콘서트 예매 서비스에서 발생할 수 있는 동시성 이슈와 처리 방법
1. 좌석 예약 (선점) 동시성 이슈
여러 사용자가 동시에 같은 좌석을 예약하려고 할 때 발생할 수 있는 문제입니다.
2. 처리 방법
a) 비관적 락 (Pessimistic Lock) 사용
SeatRepositoryCustomImpl 클래스에서 구현되어 있습니다:
@Override
public Optional<Seat> findAndLockByConcertDateIdAndSeatId(Long concertDateId, Long seatId) {
QSeat seat = QSeat.seat;
Seat foundSeat = queryFactory
.selectFrom(seat)
.where(seat.concertDateId.eq(concertDateId)
.and(seat.seatId.eq(seatId)))
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.fetchOne();
if (foundSeat != null) {
entityManager.lock(foundSeat, LockModeType.PESSIMISTIC_WRITE);
}
return Optional.ofNullable(foundSeat);
}
b) 낙관적 락 (Optimistic Lock) 사용
InventoryRepositoryCustomImpl 클래스에서 구현되어 있습니다:
@Override
@Transactional
public Long updateRemainingInventoryWithOptimisticLock(Long concertId, Long concertDateId, Long remainingChange, Long version) {
QInventory inventory = QInventory.inventory;
long updatedRows = queryFactory.update(inventory)
.set(inventory.remaining, inventory.remaining.add(remainingChange))
.set(inventory.version, inventory.version.add(1))
.where(inventory.concertId.eq(concertId)
.and(inventory.concertDateId.eq(concertDateId))
.and(inventory.version.eq(version)))
.execute();
return updatedRows;
}
c) Redis 사용
RedisRepositoryImpl 클래스에서 구현되어 있습니다. 이 메서드는 Redis의 setIfAbsent
명령을 사용하여 원자적으로 키를 설정하고 만료 시간을 지정합니다.
@Override
public boolean setTempSeat(String key, String value, Long expirationMinutes) {
return redisTemplate.opsForValue().setIfAbsent(key, value, expirationMinutes, TimeUnit.MINUTES);
}
d) Kafka 사용
ConcertConsumer 클래스에서 구현되어 있습니다.
Kafka를 통해 예약 요청 이벤트를 비동기적으로 처리하고, Redis를 사용하여 좌석 상태를 업데이트합니다:
@KafkaListener(topics = TOPIC_RESERVATION_REQUESTS)
@Transactional
public void handleReservationRequest(ReservationRequestedEvent event) {
// ... (중략)
boolean updateSuccess = redisRepository.updateSeatStatus(event.seatId(), SeatStatus.TEMP_RESERVED);
if (!updateSuccess) {
throw new CustomException(ErrorCode.SEAT_UPDATE_FAILED, "Failed to update seat status", Level.ERROR);
}
// ... (중략)
}
결론
이 프로젝트에서는 여러 가지 동시성 제어 방법을 복합적으로 사용하고 있습니다:
- 데이터베이스 레벨에서는 비관적 락과 낙관적 락을 사용하여 동시 접근을 제어합니다.
- Redis를 사용하여 분산 환경에서의 빠른 데이터 접근과 임시 예약 상태 관리를 구현합니다.
- Kafka를 통한 이벤트 기반 아키텍처로 시스템의 확장성을 높이고 비동기 처리를 가능하게 합니다.
이러한 방식을 조합하여 사용함으로써, 시스템은 높은 동시성 환경에서도 데이터 정합성을 유지하면서 효율적인 처리가 가능합니다. 다만, 여러 기술을 복합적으로 사용하기 때문에 시스템의 복잡도가 증가할 수 있으므로, 철저한 테스트와 모니터링이 필요할 것입니다.
'프레임워크 > 자바 스프링' 카테고리의 다른 글
대규모 트래픽 게시판 구축 시리즈 #1: 프로젝트 기획 및 요구 사항 (0) | 2024.09.05 |
---|---|
[spring cloud][ecommerce] 개요 & 구성 (0) | 2024.08.12 |
JPA 테스트 코드 작성시 UPDATE Query 생성이 안되네? (0) | 2024.05.28 |
JPA 연결 테스트 코드 (0) | 2024.05.28 |
[그냥 보는] application.yaml (0) | 2024.05.28 |