서론
Cloudwave에서 프로젝트가 시작되고 한 달이 조금 지나, 개발을 본격적으로 시작하게 되었다.
주제는 CJ의 대표적인 사업인 올리브영과 CGV 중에 고르는 것이었는데, 우리 조는 CGV를 고르게 되었다.
순간적으로 몰리는 대규모 트래픽을 처리하는 인프라를 설계하고 경험해보고 싶었다.

AWS 서비스에 대한 비용을 지원해주기에, AWS 클라우드 상에 인프라를 구성했다.
대규모 트래픽을 예상해 트래픽 분산처리를 위해 로드밸런서(alb)와 eks의 노드그룹 오토스케일링을 구성했고,
대규모 요청 속 예매 서비스를 안정적이게 운영하기 위해 트래픽 분산,서버 과부하 방지등을 위해 카프카와 레디스를 구성했다.
코드로 정합성 문제를 체험하는 것 부터 시작해, 락으로 정합성 문제를 해결하고 Kafka와 Redis로 대기열을 구현해 안정적인 서비스를 보장하는 인프라를 만들 예정이다.
ERD
우선 ERD는 다음과 같다.

정합성 테스트 코드
@Test
@DisplayName("좌석 예매 정합성 테스트")
void testConcurrentReservation() throws InterruptedException {
int numberOfThreads = 5;
ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
CountDownLatch latch = new CountDownLatch(numberOfThreads);
AtomicInteger successCount = new AtomicInteger(0);
List<Exception> exceptions = new ArrayList<>();
for (int i = 0; i < numberOfThreads; i++) {
final String username = "user" + i;
executorService.submit(() -> {
try {
reservationService.createReservation(username, seatId);
successCount.incrementAndGet();
} catch (Exception e) {
synchronized (exceptions) {
exceptions.add(e);
}
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
Seat seat = seatRepository.findById(seatId)
.orElseThrow(() -> new RuntimeException("Seat not found"));
System.out.println("성공한 예약 수: " + successCount.get());
System.out.println("좌석 예약 여부: " + seat.getIsReserved());
System.out.println("발생한 예외 수: " + exceptions.size());
assertTrue(seat.getIsReserved(), "좌석은 예약 상태여야 합니다");
assertEquals(1, successCount.get(), "동시에 하나만 성공해야 합니다");
assertEquals(numberOfThreads - 1, exceptions.size(), "나머지는 예외가 발생해야 합니다");
}
우선 스레드를 5개 생성해 동시에 한 좌석에 대한 예매를 진행하는 테스트를 했다.
예상한 결과는 한 좌석에 5개의 예매내역이 생기는걸 기대하고 테스트가 실패하는 걸 기대했다. 하지만..

테스트가 성공하고 예약도 하나만 되었다. 이게 무슨 일인가 하고 로그를 보니
o.h.engine.jdbc.spi.SqlExceptionHelper : Deadlock found when trying to get lock; try restarting transaction
데드락이 생겨 나머지 4개의 스레드는 트랜잭션을 롤백시켜 좌석 예매를 하지도 못한 것이다.
명시적으로 락을 설정해준 부분이 없는데 왜 데드락이 생길 까 싶어서, 찾아보니 mysql에서 자동으로
S-Lock(공유락, 읽기 O, 쓰기 X), X-Lock(배타락, 읽기 X, 쓰기 X)을 적용하고 있었다.
@Transactional
public ReservationRes createReservation(String userName, Long seatId){
Seat seat=findSeatBySeatId(seatId);
if(!seat.getIsReserved())
seat.soldout();
else throw new CustomException(StatusCode.SEAT_SOLD_OUT);
Reservation reservation= Reservation.builder()
.userName(userName)
.status(Status.RESERVED)
.seat(seat)
.build();
return ReservationRes.from(reservationRepository.save(reservation));
}
위 코드가 좌석 예매하는 서비스 로직이다.
seat.soldout();
해당 코드에서 seat 레코드에 x-lock을 걸고 트랜잭션이 끝날 때 까지 가지고 있는다.
따라서 스레드 하나만 예약이 가능하고 나머지는 x-lcok을 얻기 위해 대기하다 데드락이 발생한 것으로 보인다.
x-lock은 update,delete,insert 를 할 때 걸린다고 알고 있다.
그럼 s-lock은 언제 생길까?
위 MySQL 공식문서에 가보면 아래와 같은 문장이 있다.
If a FOREIGN KEY constraint is defined on a table, any insert, update, or delete that requires the constraint condition to be checked sets shared record-level locks on the records that it looks at to check the constraint. InnoDB also sets these locks in the case where the constraint fails.
FK 제약조건을 가지고 있는 테이블에서 Insert, Update, Delete는 제약조건이 위반되는지 확인해야 하기 때문에, 참조되는 FK 레코드에 대해 s-lock을 건다.
즉, FK 제약조건을 가지고 있는 테이블에 쿼리를 실행할 때, 해당 FK 레코드에 대해 s-lock을 건다.
reservationRepository.save(reservationRepository.save(reservation))
seatId 외래키를 가지고 있는 reservation을 insert 하므로 이 코드에서 seat 레코드에 대한 s-lock을 획득한다.
그럼 순서는 x-lock -> s-lock 순으로 획득해야하지만,
Hibernate:
insert
into
reservation
(seat_id, status, user_name)
values
(?, ?, ?)
Hibernate:
update
seat
set
column_index=?,
is_reserved=?,
row_index=?,
schedule_id=?
where
id=?
실제 쿼리는 update ->insert 순으로 진행한다.
따라서 s-lock -> x-lock 순으로 가게되고, s-lock을 가지고 있다가 해제한 뒤 x-lock을 가진다. x-lock은 읽기도 다른 트랜잭션이 불가능하게 하기때문에 s-lock을 해제해야한다.

지금 테스트는 스레드가 여러개인 상황에서 동시에 트랜잭션이 진행되고 있다.
seat 레코드에 대한 s-lock을 트랜잭션 1,2,3이 걸게 된다.(s-lock은 여러 트랜잭션이 걸 수 있다)
하지만 x-lock을 걸기 위해서는 해당 레코드에 락이 존재하지 않아야한다.
따라서 서로 s-lock이 끝날 때까지 대기 하기 때문에 데드락이 생기는 것이다.
마무리
테스트 코드를 짜서 확인해보기 전까지는, 단순히 정합성 문제가 생기기에 비관적 락이나 낙관적 락을 쓰는 것이라고 알고 있었다.
하지만, 이렇게 아무런 설정 없이 자원에 대해 동시 접근을 하게되면 데드락과 같은 문제가 생기기에, s-lock,x-lock같은 DB 수준의 락을 개발자가 직접 제어해서 사전에 예방하기 위해 비관적락, 낙관적 락을 쓰는 것이구나 하고 느꼈다.
다음 글에 직접 적용하고 둘 간의 차이점도 알아보도록 하겠다.
'Cloudwave' 카테고리의 다른 글
| [Cloudwave] CGV 대용량 트래픽 예매 서비스 구현 - 4 (0) | 2025.05.04 |
|---|---|
| [Cloudwave] CGV 대용량 트래픽 예매 서비스 구현 - 3 (0) | 2025.04.27 |
| [Cloudwave] CGV 대용량 트래픽 예매 서비스 구현 - 2 (0) | 2025.04.24 |
| Jira & Confluence 도입기 (1) | 2025.03.12 |