2026/04/03 10

SSE를 실무에 도입하면서 마주친 것들 Polling에서 SSE로, 다시 Polling으로

처음에는 Polling이 싫었다법률 번역 플랫폼을 만들 때의 일이다. 법률 문서를 업로드하면 AI가 문단별로 번역하는 시스템이었다. 원래 구조는 단순했다. 클라이언트가 5초마다 번역 상태를 폴링하고, 서버는 DB에서 현재 진행률을 읽어서 응답한다.// 원래 구조: 5초 폴링@GetMapping("/translation/{taskId}/status")public TranslationStatus getStatus(@PathVariable String taskId) { return translationRepository.findStatus(taskId);} 문제가 세 가지 있었다.첫째, 5초 간격이 너무 느렸다. 번역이 문단 단위로 진행되는데, 문단 하나가 1~2초면 끝난다. 그런데 폴링이 5초라 진행률..

운영 2026.04.03

ZADD vs ZADD NX 대기열에서 멱등성이 UX를 결정한다

플래그 하나가 사용자의 대기 순서를 바꾼다대기열 진입 API를 처음 구현했을 때, 나는 별 생각 없이 ZADD를 썼다. Redis Sorted Set에 사용자 ID를 멤버로, 현재 타임스탬프를 스코어로 넣는 단순한 구조였다.redisTemplate.opsForZSet().add("queue:waiting", userId, System.currentTimeMillis()); 동작은 했다. 대기열에 들어가고, 스코어(타임스탬프) 기준으로 정렬되고, ZPOPMIN으로 먼저 들어온 사용자부터 빠지는 구조가 완성됐다.그런데 테스트 중에 이상한 일이 발생했다. 대기열 순번이 50번이던 사용자가 브라우저를 새로고침한 뒤 150번으로 밀려나 있었다. "분명 50번이었는데" 하는 사용자의 당혹감은 서비스 신뢰도를 순간적..

운영 2026.04.03

토큰 TTL 하나로는 부족하다. Access, Activity, Hard Limit 3계층 설계

TTL 180초의 딜레마대기열을 통과한 사용자에게 토큰을 발급할 때, TTL을 몇 초로 설정할지가 생각보다 어려운 문제였다.처음에는 단순하게 180초(3분)로 잡았다. "결제까지 3분이면 되지 않나?" 그런데 실제 이커머스 사용자의 행동 패턴을 떠올려보니, 이게 단순하지 않았다.TTL = 60초? → 쿠폰을 고르다가 90초째에 토큰이 만료. 처음부터 다시 대기열.TTL = 300초? → 결제를 포기하고 탭을 닫은 사용자가 5분 동안 자리를 차지.TTL = 180초? → 둘 다 완벽히 해결하지 못하는 절충안. 핵심 테제: 단일 TTL은 "활발한 사용자"와 "이탈한 사용자"를 구분할 수 없다. 이 두 가지를 동시에 다루려면 TTL을 계층화해야 한다.이커머스 사용자 행동 스펙트럼토큰 TTL을 설계하려면, 먼..

대기열 폴링 최적화 동적 간격으로 QPS 68%를 줄이다

10,000명이 1초마다 물어본다대기열을 구현하고 나서, 처음 마주한 질문은 "사용자가 자기 순번을 어떻게 알지?"였다. 가장 직관적인 답은 폴링이다. 클라이언트가 주기적으로 서버에 "내 순번 몇 번이야?"를 묻는 것.문제는 숫자에 있다.10,000명 대기 중 x 1초마다 요청 = 10,000 QPSRedis가 처리 못 하는 수준은 아니지만, 99%는 낭비다.9,500번째 사용자가 매초 순번을 확인해봐야 "9,498번째요"가 "9,497번째요"로 바뀌는 것뿐이다. 솔직히 처음에는 "Redis가 초당 10만 연산 처리하니까 10,000 QPS쯤이야"라고 생각했다. 그런데 이건 Redis만의 문제가 아니다. 10,000 QPS면 Spring 서버도 초당 10,000건의 HTTP 요청을 받아야 하고, Hika..

운영 2026.04.03

@Scheduled가 3대에서 돌면 생기는 일: 분산 환경 스케줄러의 함정

테스트에서 사라진 30명동시성 테스트를 돌리고 있었다. 100명이 대기열에 등록하고, 전원이 활성화된 뒤 주문하는 시나리오였다. 그런데 결과가 계속 70명만 성공했다.100 - 70 = 30. 배치 사이즈가 30이었다.처음에는 동시성 문제를 의심했다. 락 충돌인가? 커넥션 풀 부족인가? 한참을 뒤지다가 원인을 찾았다. @SpringBootTest가 @Scheduled 빈을 자동으로 시작하고, 테스트 실행 중에 스케줄러가 대기열에서 30명을 꺼내가고 있었다. 테스트가 100명을 등록하고 읽으려는 사이에 스케줄러가 30명을 소비해버린 것이다.이건 로컬에서의 문제였지만, 프로덕션에서는 더 심각한 버전이 존재한다. 서버가 3대면 스케줄러도 3개다.@Scheduled의 본질: JVM 로컬 타이머@Scheduled..

Fail-Open vs Fail-Closed: Redis가 죽으면 매출을 포기할 것인가

Redis가 죽는 순간을 상상해본 적 있는가대기열 시스템을 만들고 나서, 나는 한 가지 질문에 부딪혔다. "Redis가 죽으면 어떻게 하지?"이커머스 대기열의 모든 흐름이 Redis를 경유한다. 대기열 등록은 Sorted Set에 ZADD, 활성화는 ZPOPMIN, 토큰 검증은 GET. Redis가 응답하지 않으면 아무도 주문할 수 없다.처음에 나는 당연히 "Redis가 죽으면 주문을 막아야지"라고 생각했다. 무결성이 깨질 수 있으니까. 그런데 이 "당연한" 선택이 비즈니스 관점에서 정말 당연한지 곰곰이 따져보니, 그렇지 않았다.결론부터 말하면: 이커머스에서는 Fail-Open을 선택했고, RateLimiter의 허용 수치를 배치 사이즈 M=30과 동일하게 설정한 것이 설계의 핵심이다.Fail-Close..

아키텍처 2026.04.03

Fixed-Rate vs Concurrency-Based 대기열 활성화 전략을 도메인이 결정한다

처음에 당연하다고 생각했던 것대기열 시스템을 구현하면서, 나는 처음에 "활성 사용자 수를 모니터링하다가 자리가 비면 새 사용자를 넣으면 되지 않나?"라고 생각했다. 은행 번호표처럼. 누군가 창구를 떠나면 다음 사람을 부르는 방식. 직관적이고 자연스럽다.그런데 실제로 이커머스 주문 대기열에 이 방식을 적용하려고 하니, 생각보다 근본적인 문제가 드러났다. 결론부터 말하면: 대기열 활성화 전략은 기술적 우열이 아니라 도메인의 체류 시간 특성이 결정한다.두 가지 전략의 본질대기열에서 대기 중인 사용자를 활성 상태로 전환하는 방식은 크게 두 가지다.Fixed-Rate (고정 속도 방식)매 스케줄러 주기마다 정확히 M명을 활성화한다. 현재 활성 사용자가 몇 명인지는 보지 않는다. 놀이공원 입장 게이트처럼 — 안에 ..

아키텍처 2026.04.03

Rate Limiting 알고리즘 전체 지도 - Token Bucket부터 Sliding Window까지

처음에 30이라는 숫자만 있었다대기열 시스템의 Fail-Open 폴백을 설계하면서 Resilience4j의 RateLimiter를 도입했다. Redis가 죽었을 때 DB를 보호하기 위한 마지막 방어선이었고, 설정은 이랬다.RateLimiterConfig.custom() .limitForPeriod(30) // 1초에 30개 .limitRefreshPeriod(Duration.ofSeconds(1)) .timeoutDuration(Duration.ZERO) // 대기 없이 즉시 거부 .build(); 30이라는 숫자는 DB TPS에서 역산한 값이다. HikariCP 풀 10개, 주문 처리 약 200ms, 그래서 초당 처리 가능한 요청이 대략 50개. 여기서 안전 마진을..

아키텍처 2026.04.03

WIL - 8주차 (개선은 문제를 정확히 말하는 데서 시작된다)

이번 주에 새로 배운 것"왜 30인지" 설명할 수 있어야 설계다대기열 시스템을 구현하면서 스케줄러의 배치 크기 M을 정해야 했다. 처음에는 "30이면 적당하지 않을까?"라고 감으로 잡으려 했다. 하지만 왜 30인지, 25가 아니고 50이 아닌 이유를 설명할 수 있어야 비로소 설계라고 부를 수 있다.DB 커넥션 풀(10개)에서 출발해서 커넥션 점유 시간(200ms)으로 이론 TPS(50)를 구하고, 안전 마진 60%를 적용해서 실효 TPS 30을 얻었다. 이 값이 스케줄러 주기 1초와 만나서 M=30이 됐다.숫자에 근거가 생기니까 달라지는 게 있었다. "놀이공원식(Fixed-Rate)이 왜 맞는가"에 대해 M이 TPS 기반이므로 누적 위험이 보수적으로 통제된다고 설명할 수 있었고, Redis 장애 시 Ra..

스터디/루퍼스 2026.04.03

대기열 배치 크기를 방정식으로 산정하기까지

대기열 스케줄러의 배치 크기를 "감"이 아닌 "방정식"으로 산정하기까지TL;DR대기열 스케줄러가 1초마다 몇 명을 활성화할지를 정하는 건 감이 아니라 역산이다. DB 커넥션 풀에서 출발해서 TPS를 구하고, 안전 마진을 적용하면 "왜 이 값인지"를 설명할 수 있는 숫자가 나온다. 그리고 이 숫자가 방정식에서 나왔다는 사실이, 설계 판단을 설명 가능하게 만드는 핵심 근거가 된다.대기열이 보호하는 것쇼핑몰에서 블랙프라이데이를 한다고 가정해보자. 평소에 초당 100명 정도가 접속하던 사이트에 갑자기 10만 명이 동시에 몰린다. 전원이 "주문하기" 버튼을 누른다.서버가 요청을 받는 것 자체는 가능하다. Tomcat은 수천 개의 동시 연결을 처리할 수 있다. 문제는 주문을 처리하는 과정에서 생긴다. 주문을 처리하..

운영 2026.04.03