Java 9

[동시성 실험] Queue를 붙이면 예약 API가 빨라질 줄 알았다 -9

Queue를 붙이면 예약 API는 빨라질 거라고 생각했다. 앞의 실험에서는 같은 좌석에 요청이 몰릴 때마다 누군가 기다려야 했다. 비관적 락은 DB lock 앞에서 기다렸고, Redis 분산 락은 Redis key 앞에서 기다렸다. 특히 Redis 분산 락의 정상 멀티 실험은 중복 예약을 막았지만 p95가 7.49초였다. watchdog 조건에서는 p95가 12.60초까지 올라갔다. 그래서 마지막 6단계에서는 기다리는 위치를 바꿔보기로 했다.사용자의 HTTP 요청이 예약 완료까지 기다리지 않고, 일단 Kafka에 요청을 넣은 뒤 consumer가 나중에 처리하도록 만들었다. API는 접수만 하고 끝나니 응답도 금방 내려갈 것 같았다. 측정 전 예상은 enqueue p95 300ms 이하였다.실제 정상 부..

JavaSpring 2026.07.11

[동시성 실험] Redis 락도 만료되면 깨진다 -8

이전 글에서는 Redis 분산 락이 정상일 때를 봤다.결과는 괜찮았다. 단일 인스턴스에서도, 멀티 인스턴스에서도 성공 예약은 1건이었다. 초과 성공은 0건이었다. synchronized와 달리 JVM이 여러 개여도 같은 Redis key를 바라보니까 멀티 인스턴스 중복 예약을 막을 수 있었다.하지만 정상 상황만 보면 Redis lock의 절반만 본 것이다.운영에서 더 중요한 질문은 따로 있다.Redis가 죽으면 어떻게 될까?그리고 하나 더.Redis lock을 잡았는데, 처리 도중 lock TTL이 먼저 끝나면 어떻게 될까?이번 글에서는 그 실패 조건을 본다.먼저 용어부터 정리하자Redis lock을 볼 때 헷갈리기 쉬운 값이 세 개 있었다.waitTime, leaseTime, watchdog이다.첫 번..

JavaSpring 2026.07.05

[동시성 실험] Redis 분산 락을 쓰면 멀티 인스턴스 문제를 해결할 수 있을까? -7

앞의 실험들에서 계속 같은 질문으로 돌아왔다. 앱 인스턴스가 여러 대일 때, 같은 좌석에 대한 요청을 어디서 줄 세워야 할까?2단계에서 synchronized를 써봤지만 멀티 인스턴스에서는 깨졌다. JVM이 두 개면 lock도 두 개였기 때문이다.3단계에서 비관적 락을 써봤다. DB의 SELECT ... FOR UPDATE로 같은 좌석 row를 잠그니까 단일과 멀티 모두 중복 예약은 막혔다. 하지만 락 대기와 커넥션 풀 병목이 같이 따라왔다.4단계에서 낙관적 락도 봤다. @Version을 붙였지만 인기 좌석에서는 예상처럼 "전부 버전 충돌"이 나지 않았다. 오히려 FK 때문에 생긴 S-lock -> X-lock deadlock이 더 크게 보였다. 그래서 5단계에서는 락을 DB 밖으로 빼보기로 했다.Red..

JavaSpring 2026.06.28

[동시성 실험] 낙관적 락으로 막으면 다 버전 충돌일 줄 알았는데, 정작 deadlock이 터졌다 -6

지난 3단계에서 비관적 락(SELECT ... FOR UPDATE)을 써봤다. 정합성은 완벽했는데, 한 가지가 계속 걸렸다.충돌이 하나도 없어도 일단 락부터 잡고 들어간다는 점이다. 인기 좌석이든 아무도 안 보는 좌석이든, 모든 예약이 줄을 서서 X-lock을 잡고 commit까지 들고 있었다.그래서 4단계는 낙관적 락(@Version)으로 넘어왔다. 낙관적 락의 아이디어는 단순하다."일단 충돌 안 날 거라고 보고 그냥 진행하다가, 커밋할 때 누가 먼저 바꿨으면 그때 실패시킨다."선점 락을 안 잡으니까, 충돌이 없을 땐 비관적 락보다 쌀 거라는 기대였다.처음 예상시나리오는 두 개로 잡았다.시나리오 A: 좌석 1개에 2,000명이 동시에 (인기 좌석, 고경합)시나리오 B: 좌석 100개에 2,000명을 분..

JavaSpring 2026.06.14

[동시성 실험] 비관적 락을 걸었더니 중복은 막혔고, 병목은 다른 곳에서 보였다 -5

앞의 두 실험은 예상과 꽤 다르게 흘러갔다. 1단계 No Lock에서는 중복 예약이 터질 줄 알았다. 그런데 중복 예약은 생기지 않았고, 대신 MySQL InnoDB의 deadlock이 나왔다.2단계에서는 synchronized를 걸었다. 단일 JVM에서는 한 번에 한 요청만 들어오니까 이제 안전할 줄 알았다. 하지만 @Transactional commit이 synchronized lock 해제 뒤에 일어나면서 race window가 생겼고, 성공 예약이 2건 남았다. 멀티 인스턴스에서는 더 직접적으로 깨졌다. JVM이 두 개면 synchronized lock도 두 개였기 때문이다. 그래서 3단계에서는 락의 위치를 애플리케이션 밖으로 옮겼다.DB에 락을 건다. 여기서부터는 락 전략이 크게 두 갈래로 나뉜..

JavaSpring 2026.06.03

[동시성 실험] synchronized는 JVM이 두 개면 어떻게 깨질까? -4

앞 글에서는 시나리오 1, 단일 인스턴스에서 synchronized를 걸어봤다. 결과는 예상보다 복잡했다.JVM 하나 안에서는 메서드 진입이 직렬화되면서 deadlock은 줄었다. 하지만 @Transactional 프록시 구조 때문에 synchronized lock이 풀린 뒤 commit 전 race window가 열렸고, 성공 예약이 2건 생겼다.이번 글에서는 시나리오 2를 본다.멀티 인스턴스다.k6 → nginx → Spring JVM 2개 → MySQL 운영 환경에서는 앱을 하나만 띄우지 않는 경우가 많다.트래픽을 나누거나 무중단 배포를 하려면 같은 Spring 앱을 여러 개 띄우고, 앞단의 로드밸런서가 요청을 분산한다.문제는 synchronized가 JVM 안에서만 통한다는 점이다.Spring 앱..

JavaSpring 2026.05.31

[동시성 실험] synchronized는 단일 JVM에서도 안전할까? -3

1단계에서는 락을 일부러 걸지 않았다. 그런데 중복 예약은 생기지 않았다. 대신 MySQL InnoDB의 row-level lock과 FK 검증이 얽히면서 deadlock이 발생했다.처음에는 이 결과를 보고 이렇게 생각했다.그럼 애플리케이션에서 먼저 직렬화하면 되지 않을까? 그래서 2단계에서는 가장 단순한 방법부터 써봤다.synchronized Java 메서드에 키워드 하나만 붙이면, 같은 JVM 안에서는 한 번에 한 스레드만 그 메서드에 들어갈 수 있다.좌석 예약처럼 "한 번에 하나만 처리하면 되는" 로직에는 꽤 그럴듯해 보인다.이번 글에서는 먼저 시나리오 1, 단일 인스턴스 실험만 본다.k6 → Spring JVM 1개 → MySQL처음 예상은 단순했다.단일 JVM에서는 synchronized가 요청..

JavaSpring 2026.05.31

[카데미] 화려한 기능 하나 없이 한 달을 보낸 이유

새로고침 한 번에 로그아웃되던 첫 한 달첫 커밋을 찍고, 그 후 약 3주 동안 한 일은 거의 다 "골격 잡기"였다.데이터 모델부터뭐든지 처음은 데이터 모델이다. 단순하게 잡았다.학습지(Worksheet): 사용자가 만드는 카드 모음의 단위. 제목, 설명, 소유자.카드(Item): 학습지에 속하는 한 장의 카드. 앞면(질문), 뒷면(답), 부가 설명.숙달(Mastery): 카드별로 사용자가 외웠는지 여부. boolean 하나면 충분했다.ERD를 그려서 막 정교하게 짜지는 않았다. "일단 굴러가는 모양"이면 됐고, 모자라면 마이그레이션 한 번 더 치면 된다.User (1) ── (*) Worksheet └── (*) Item └── (1:1) Item..

[동시성 실험] 티켓팅 동시성 문제, 직접 실험해보기로 했다 -1

좌석 예약 시스템을 보면 늘 궁금한 게 있었다."인기 공연 티켓팅처럼 사람들이 한 좌석에 동시에 몰리면, 서버 안에서는 정확히 무슨 일이 벌어질까?"겉으로 보면 간단해 보인다. 좌석 하나를 조회하고, 비어 있으면 예약 처리하고, 아니면 실패를 돌려주면 된다. 그런데 문제는 "동시에"다. 한 명씩 차례대로 들어오면 별일이 없지만, 1,000명이 거의 같은 순간에 같은 좌석을 누르면 이야기가 달라진다.이번 실험의 목표는 실제 예약 서비스를 완성하는 게 아니다. CRUD 기능을 하나 더 만드는 것도 아니다. 목표는 더 단순하고, 그래서 더 어렵다.동시 요청에서 예약 정합성이 어떻게 깨지는지 직접 재현하고, 각 해결 전략이 어떤 대가를 치르는지 확인하는 것.실험의 출발점이번 프로젝트에서 다룰 핵심 시나리오는 하..

JavaSpring 2026.05.21
반응형