전체 글 176

[WIL] - Round8. 대기열 유입 제어, 순서 보장, 상태 전이

대기열은 트래픽을 줄이기 위한 도구가 아니라, 시스템이 감당할 수 있는 속도로 요청을 흘려보내는 도구다.그래서 대기열은 기능이 아니라 '유입 제어', '순서 보장', '상태 전이'의 세 축으로 보는 편이 안전하다.1. 유입 제어유입 제어의 핵심은 시스템이 감당할 수 있는 처리량을 먼저 정하고, 그 속도에 맞춰 요청을 흘려보내는 일이다.1-1. 처리량 상한은 병목 자원에서 정한다유입 제어를 설계할 때 가장 먼저 할 일은 아래다.시스템에서 가장 먼저 한계에 도달하는 자원이 무엇인가그 자원의 이론적 최대 처리량은 얼마인가안전 마진을 빼면 실제 목표 처리량은 얼마인가보통 병목은 커넥션 풀, 외부 API 호출, 디스크 쓰기 중 하나다.예시 코드// 병목: 커넥션 풀 50개, 건당 처리 시간 200msint max..

[Waiting Queue] 당신의 대기열은 안녕하신가?

TL;DR대기열에서는 실시간 순번, 부하 분산, 중복 진입 등의 문제가 발생한다. 각각의 문제를 해결하기 위한 보편적인 방법으로 SSE, Jitter, NX와 같은 방법이 있지만, 데이터 특성과 흐름을 생각해보면 예상외의 결과가 발생한다.- 순번은 1:N fan-out이라 SSE보다 Polling이 자연스럽다.- 대기열을 통과한 유저를 거부하는 Jitter/Rate Limit은 대기열의 약속을 깨뜨린다.- ZADD NX를 끄면 코드 한 줄 없이 트래픽을 1/5로 줄일 수 있다.- 순번 정밀도를 한 단계 낮추면 Redis 장애에도 서비스가 유지된다.즉, '좁은 범위의 특정 문제'만을 바라보지 말고, 전체적인 서비스의 흐름과 유저 행동을 먼저 봐야 한다. 티켓팅을 해본적이 있는가?아주 한정적인 짧은 시간동..

[WIL] - Round7 (3/3). 선착순 쿠폰 발급

수량 제한이 있는 자원을 대량 요청에서 정확하게 분배하는 문제는 동시성 제어의 대표적인 시나리오다. 선착순 쿠폰 발급은 이 문제를 Kafka 파이프라인 위에서 해결하는 사례다.동시성 제어 방식 선택즉시 판정 vs 비동기 처리수량 제한이 있는 발급에서 동시성을 제어하는 방식은 크게 두 가지로 나뉜다.즉시 판정 방식은 아래 특성을 가진다.요청 시점에 발급 가능 여부를 즉시 응답한다Redis 같은 인메모리 저장소로 원자적 카운터를 구현한다사용자 경험이 좋다. 요청 즉시 성공/실패를 알 수 있다동시성 제어가 복잡하다. 분산 환경에서 원자적 연산을 보장해야 한다적합한 상황: 재고 차감처럼 즉시 결과가 필요한 경우비동기 처리 방식은 아래 특성을 가진다.요청 접수만 보장하고, 실제 처리는 나중에 한다메시지 큐의 순차..

[WIL] - Round7 (2/3). Kafka 파이프라인

in-process 이벤트는 같은 애플리케이션 내에서만 동작한다. 시스템 간 이벤트 전파가 필요하면 메시지 브로커를 도입해야 한다. 이때 가장 큰 문제는 '도메인 변경은 저장됐는데 이벤트 발행은 실패하는 상황'과 '같은 이벤트가 두 번 처리되는 상황'이다.발행 보장 — Transactional Outbox 패턴문제 구조도메인 변경과 이벤트 발행이 별개의 시스템에서 이루어지면 원자성을 보장할 수 없다. 데이터베이스에 주문은 저장됐는데 Kafka 발행이 실패하면 소비자는 주문이 생성된 사실을 알 수 없다.두 가지 접근이 가능하다.이벤트를 먼저 발행하고 도메인 변경을 나중에 한다 → 발행은 됐는데 저장이 실패하면 존재하지 않는 주문에 대한 이벤트가 돌아다닌다도메인 변경을 먼저 하고 이벤트를 나중에 발행한다 →..

[WIL] - Round7 (1/3). 이벤트 분리 기준

[WIL] - Round7 (1/3). 이벤트 분리 기준이벤트 기반 아키텍처는 세 축으로 나눠 정리한다.1편 — 이벤트 분리 기준: 어떤 로직을 이벤트로 분리할지, 리스너와 트랜잭션 경계를 어떻게 설계할지2편 — Kafka 파이프라인: Outbox 패턴으로 발행을 보장하고, Consumer에서 멱등 처리·순서 보장·배치·DLQ·2단 구조를 구현하는 방법 (https://kdh0518.tistory.com/186)3편 — 선착순 쿠폰 발급: 비동기 처리 기반 동시성 제어, 단일 파티션 순차 처리, polling 결과 확인 구조 (https://kdh0518.tistory.com/187)단일 트랜잭션에 모든 로직을 묶는 구조는 단순하지만, 부수효과 하나의 실패가 핵심 로직까지 롤백시키는 문제를 만든다. 이벤..

[Event] 머리 박으면서 배운 '이벤트'에 대한 4가지 깨달음

TL;DR이벤트는 silver bullet이 아니다.결합도를 낮추는 데는 분명 유용하지만, 경계를 자동으로 지켜주지도 않고, 정합성을 저절로 보장해주지도 않는다. 결국 중요한 것은 이벤트를 얼마나 많이 썼는지가 아니라, 어떤 기준으로 남기고 버릴지를 정하는 일이다. 나는 이벤트를 접하기 전, 아키텍처에 대해 많은 고민을 해왔었다.BC 간 강결합을 줄이고 싶었고, 주요 비즈니스 로직과 부가 로직을 분리하고 싶었다. 장애가 한 곳에서 나더라도 다른 곳으로 쉽게 번지지 않는 구조를 만들고 싶기도 했다. 그리고 Event를 알고 난 뒤에는 이벤트가 모든 문제를 해결해줄 수 있는 것 처럼 보였고, 적극적으로 사용하려했다. 그런데 정말로 이벤트가 이 모든 문제들을 해결해주었을까? 물론 언급했던 문제들을 말끔하게 ..

[WIL] - Round6. 결제 흐름, 실패 처리, 비동기 복구

외부 결제 시스템과 연동하는 서비스는 요청이 성공하는 경우보다 실패하는 경우를 더 많이 고려해야 한다.요청이 거부될 수도 있고, 응답이 늦을 수도 있고, 응답 자체가 오지 않을 수도 있다.그래서 결제 연동의 핵심은 '정상 흐름을 만드는 일'이 아니라 '실패했을 때 시스템이 어떻게 버틸 것인가'를 설계하는 일에 가깝다.이 글은 외부 결제 시스템 연동을 세 축으로 정리한다.호출 흐름: 비동기 결제에서 상태를 어떻게 관리하는가실패 처리: 타임아웃, 재시도, 서킷브레이커를 어떻게 조합하는가복구 전략: 콜백이 오지 않을 때 어떻게 최종 일관성을 확보하는가1. 호출 흐름비동기 결제의 핵심은 요청과 처리가 분리되어 있다는 점이다.요청을 보내면 즉시 결과가 오지 않는다. 결과는 나중에 콜백으로 도착하거나, 조회 API..

[CircuitBreaker] window size가 있어도 minimumNumberOfCalls가 필요한 이유

TL;DR'slidingWindowSize'는 최근 어디까지 볼지를 정하는 값이고, 'minimumNumberOfCalls'는 언제부터 그 숫자를 믿고 판단할지를 정하는 값이다.TIME_BASED에서는 특히 호출 수가 적은 구간이 생길 수 있어서, 'window'와 'minimum call'을 따로 봐야 한다. 개발하다 보면 이름은 비슷한데, 실제로는 다른 문제를 푸는 설정을 만나게 된다. 이번에 가장 헷갈렸던 값이 'slidingWindowSize'와 'minimumNumberOfCalls'였다. 처음엔 둘 다 실패율 계산에 관여하니 거의 같은 축의 설정처럼 보였다. 여기서 의문이 들었다. "window가 있는데 minimum call이 왜 또 필요하지?" 이번 글에서는 그 차이를 개념과 검증 흐름으..

[WIL] Round5. 캐시 설계 (읽기 전략, 갱신 전략, 장애 대응)

캐시는 빠르게 만들기 위한 도구이지만, 잘못 다루면 오히려 느리고 불안정한 구조를 만든다.그래서 캐시는 기능이 아니라 '읽기 전략', '갱신 전략', '장애 대응'의 세 축으로 보는 편이 안전하다.1. 읽기 전략읽기 전략의 핵심은 무엇을 어디서 읽을 것인가를 분명히 하는 일이다.1-1. 기준 원본과 조회용 데이터를 분리한다먼저 정해야 하는 것은 아래 네 가지다.정답을 가지는 기준 원본은 무엇인가캐시에 올릴 조회용 데이터는 무엇인가어떤 요청을 캐시할 것인가어떤 요청은 캐시하지 않을 것인가캐시는 기준 원본이 아니다.캐시는 기준 원본을 빠르게 보여주기 위한 복제본이다.조회가 복잡하다면 조회용 데이터를 별도로 두는 편이 낫다.여러 저장소를 자주 묶어 읽는 경우같은 계산을 반복하는 경우목록과 상세가 비슷한 데이터..

[Cache] 최선의 evict 패턴을 찾아서

TL;DR이 글의 핵심은 '더 좋은 evict 패턴을 찾는 것'이 아니다.페이지 DTO를 통째로 캐싱하는 구조에서는, 어떤 evict를 붙여도 고빈도 write 앞에서 결국 무너질 수밖에 없다.그래서 답도 eviction 최적화가 아니다. 캐시를 2계층(ID 리스트 + 상품 상세)으로 나누고, evict 대신 write-through + TTL 안전망으로 바꾸는 것이 근본 해법이었다. 이커머스 프로젝트에서 '조회' 로직의 성능 개선을 위해 캐시를 도입하고 있는데, 브랜드명 수정 로직을 보다가 누락된 지점을 하나 발견했다. 분명 '상품의 브랜드명을 수정'했는데, '상품 상세 캐시'는 그대로 남아 있는 것이다. 처음에는 'brandId 에 해당하는 상품 캐시만 찾아서 지우면 되는 거 아닌가?' 라고 정말 ..