fos-blog/study
01 / 홈02 / 카테고리
01 / 홈02 / 카테고리

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • langgraph 페이지로 이동
    • agents.md
    • BMAD Method — AI 에이전트로 애자일 개발하는 방법론
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code를 5주 더 쓴 결과 — 스킬·CLAUDE.md를 키워가는 방식
    • Claude Code를 11일 동안 쓴 결과 — 데이터로 본 나의 사용 패턴
    • Claude Code 멀티 에이전트 — Teams
    • AI 에이전트와 디자인의 새 컨벤션 — DESIGN.md, Google Stitch, Claude Design
    • 하네스 엔지니어링 실전 — 4인 에이전트 팀으로 코딩 파이프라인 구축하기
    • 하네스 엔지니어링 — 오래 실행되는 AI 에이전트를 위한 설계
    • 멀티모달 LLM (Multimodal Large Language Model)
    • AI 에이전트와 함께 MVP 만들기 — dooray-cli 사례
  • ai 페이지로 이동
    • agent 페이지로 이동
  • algorithm 페이지로 이동
    • live-coding 페이지로 이동
    • 분산 계산을 위한 알고리즘
  • architecture 페이지로 이동
    • [초안] 시니어 백엔드를 위한 API 설계 실전 스터디 팩 — REST · 멱등성 · 페이지네이션 · 버전 전략
    • [초안] API Versioning과 Backward Compatibility: 시니어 백엔드 관점 정리
    • 캐시 설계 전략 총정리
    • [초안] CJ푸드빌 커머스/F&B 도메인 설계 면접 대비 — 슬롯 경험을 주문·결제·쿠폰·매장 상태 설계로 번역하기
    • [초안] 커머스 Spring 서비스에 Clean/Hexagonal Architecture를 실용적으로 적용하기
    • [초안] 커머스 주문 상태와 데이터 정합성 기본기 — CJ푸드빌 면접 대비
    • [초안] 쿠폰/프로모션 동시성과 정합성 기본기 — 선착순·중복 사용 방지·발급/사용/복구
    • [초안] DDD와 도메인 모델링: 시니어 백엔드 관점의 전술/전략 패턴 실전 가이드
    • [초안] Decorator & Chain of Responsibility — 행동을 체인으로 조립하는 두 가지 방식
    • 디자인 패턴
    • [초안] 분산 아키텍처 완전 정복: Java 백엔드 시니어 인터뷰 대비 실전 가이드
    • [초안] 분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가
    • 분산 트랜잭션
    • [초안] e-Commerce 주문·결제 도메인 모델링: 상태머신, 멱등성, Outbox/Saga 실전 정리
    • [초안] F&B 쿠폰·프로모션·멤버십·포인트 설계
    • [초안] F&B · e-Commerce 디지털 채널 도메인 한 장 정리 — CJ푸드빌 디지털 채널 백엔드 면접 대비
    • [초안] F&B 주문/매장/픽업 상태머신 설계 — CJ푸드빌 디지털 채널 백엔드 관점
    • [초안] F&B 이커머스 결제·환불·정산 운영 가이드
    • [초안] Hexagonal / Clean Architecture를 Spring 백엔드에 적용하기
    • [초안] 대규모 커머스 트래픽 처리 패턴 — 1,600만 고객과 올영세일을 버티는 설계
    • [초안] 레거시 JSP/jQuery 화면과 신규 API가 공존하는 백엔드 운영 전략
    • [초안] MSA 서비스 간 통신: Redis Cache-Aside × Kafka 이벤트 하이브리드 설계
    • [초안] Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • [초안] Outbox / Inbox Pattern 심화 — 분산 메시징의 정합성 문제를 DB 트랜잭션으로 풀어내기
    • [초안] 결제 도메인 멱등성과 트랜잭션 재시도 기본기
    • [초안] 시니어 백엔드를 위한 Resilience 패턴 실전 가이드 — Timeout, Retry, Circuit Breaker, Bulkhead, Backpressure
    • [초안] REST API 버저닝과 모바일 앱 하위 호환성 — CJ푸드빌 디지털 채널 백엔드 관점
    • [초안] Strategy Pattern — 분기문을 없애는 설계, 시니어 백엔드 인터뷰 핵심 패턴
    • [초안] 시니어 백엔드를 위한 시스템 설계 입문 스터디 팩
    • [초안] 템플릿 메서드 패턴 - 백엔드 처리 골격을 강제하는 가장 오래되고 가장 위험한 패턴
    • [초안] 대규모 트래픽 중 무중단 마이그레이션 — Feature Flag + Shadow Mode 실전
  • database 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • redis 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • [초안] JPA N+1과 커머스 조회 모델: 주문/메뉴/쿠폰 도메인에서 살아남기
    • [초안] MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • [초안] MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • observability 페이지로 이동
    • [초안] 커머스/F&B 채널 장애 첫 5분과 관측성 기본기
    • Envoy Proxy
    • [초안] F&B / e-Commerce 운영 장애 대응과 모니터링 — 백엔드 관점 정리
    • Graceful Shutdown
  • finance 페이지로 이동
    • industry-cycle 페이지로 이동
    • investing 페이지로 이동
    • stock-notes 페이지로 이동
  • http 페이지로 이동
    • HTTP Connection Pool
  • interview 페이지로 이동
    • [초안] AI 서비스 팀 경험 기반 시니어 백엔드 면접 질문 뱅크 — Spring Batch RAG / gRPC graceful shutdown / 전략 패턴 / 12일 AI 웹툰 MVP
    • [초안] CJ푸드빌 디지털 채널 Back-end 개발자 직무 분석
    • [초안] CJ푸드빌 디지털 채널 Back-end 면접 답변집 — 슬롯 도메인 경험을 커머스/F&B 설계로 번역하기
    • [초안] F&B / e-Commerce 운영 모니터링과 장애 대응 인터뷰 정리
    • Observability — 면접 답변 프레임
    • [초안] 시니어 Java 백엔드 면접 마스터 플레이북 — 김병태
    • [초안] NSC 슬롯팀 경험 기반 질문 은행 — 도메인 모델링·동시성·성능·AI 협업
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • [초안] Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • [초안] JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • npm vs pnpm — 어떤 기준으로 선택했나
    • `setImmediate()`
  • kafka 페이지로 이동
    • [초안] Kafka 기본 개념 — 토픽, 파티션, 오프셋, 복제
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • [초안] Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • 메시지 전송 신뢰성
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • network 페이지로 이동
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • rabbitmq 페이지로 이동
    • [초안] RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • [초안] RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • security 페이지로 이동
    • [초안] 시니어 백엔드를 위한 보안 / 인증 스터디 팩 — Spring Security, JWT, OAuth2, OWASP Top 10
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • testing 페이지로 이동
    • [초안] 시니어 Java 백엔드를 위한 테스트 전략 완전 정리 — 피라미드부터 TestContainers, 마이크로벤치, Contract까지
  • travel 페이지로 이동
    • 오사카 3박 4일 일정표: 우메다 쇼핑, USJ, 난바·도톤보리, 오사카성
  • web 페이지로 이동
    • [초안] HTTP / Cookie / Session / Token 인증 기본기 — 레거시 JSP와 모바일 API가 공존하는 백엔드 관점
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/java/[초안] Java 동시성 락 정리 — 커머스…
java

[초안] Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점

커머스 백엔드에서 메뉴, 프로모션, 매장 운영 정책 같은 "거의 안 바뀌지만 모든 요청이 읽는" 데이터는 거의 예외 없이 메모리 캐시로 들어간다. 트래픽이 큰 시간대에 이 캐시를 어떻게 갱신할지가 곧 시스템의 안정성을 결정한다. 갱신 순간에 락을 잘못 잡으면 모든 조회 스레드가 멈추고, 락을 너무 느슨하게 풀면 절반은 옛 데이터, 절반은 새 데이터를 보는...

2026.05.08·9 min read·2 views

왜 지금 이 주제인가

커머스 백엔드에서 메뉴, 프로모션, 매장 운영 정책 같은 "거의 안 바뀌지만 모든 요청이 읽는" 데이터는 거의 예외 없이 메모리 캐시로 들어간다. 트래픽이 큰 시간대에 이 캐시를 어떻게 갱신할지가 곧 시스템의 안정성을 결정한다. 갱신 순간에 락을 잘못 잡으면 모든 조회 스레드가 멈추고, 락을 너무 느슨하게 풀면 절반은 옛 데이터, 절반은 새 데이터를 보는 일관성 사고가 난다.

CJ푸드빌 같은 외식 커머스 환경에서는 점심 직전과 저녁 직전 트래픽 피크 직전에 운영자가 메뉴 가격, 품절 여부, 할인율을 바꾸는 패턴이 흔하다. "변경은 분당 수 건, 조회는 초당 수천 건"이라는 비대칭이 핵심이다. 이때 단순 synchronized로 막아 버리면 운영자 1명의 메뉴 갱신이 점심 피크의 모든 조회를 줄 세우는 사고가 난다. 시니어 백엔드 면접에서 동시성 락 질문이 들어오면 대부분의 답이 synchronized vs ReentrantLock까지로 끝나는데, 실제 차별점은 "왜 read-heavy 캐시에서는 RWLock이 모자라고 StampedLock의 optimistic read가 필요한가"를 설명할 수 있느냐다.

핵심 개념 정리

thread safety의 두 축

가장 먼저 분리해야 할 두 가지 축이 있다.

  • 가시성(visibility): 한 스레드가 쓴 값이 다른 스레드에게 보이는가. volatile, 동기화 블록, final 필드 초기화 후 publish 같은 수단이 해결한다.
  • 원자성(atomicity): "읽고 비교하고 쓰는" 일련의 연산이 다른 스레드의 개입 없이 한 덩어리로 끝나는가. synchronized, 명시적 Lock, Atomic* CAS 연산이 해결한다.

volatile은 단일 변수의 가시성만 보장하고 복합 연산의 원자성은 보장하지 않는다. 캐시 객체 전체를 통째로 갈아 끼우는 패턴에서는 volatile 하나로 충분할 수 있지만, "맵 안의 한 항목만 수정"에는 절대 부족하다. 이 구분이 면접에서 가장 자주 헷갈리는 지점이다.

synchronized

JVM 내장 모니터 락. 진입/이탈이 자동이고 구현이 간단하지만, 다음 한계가 있다.

  • 읽기/쓰기를 구분하지 않는다. 100개의 스레드가 동시에 같은 캐시를 읽기만 해도 줄을 선다.
  • 인터럽트가 어렵고, tryLock 같은 비차단 시도가 없다.
  • 락 획득 대기 시간이 길어지면 throughput이 급격히 떨어진다.

상태가 거의 안 바뀌고 호출 빈도가 낮은 영역(예: 초기화 가드, 카운터 증가)에 한정해서 쓴다.

ReentrantLock

synchronized의 기능 확장판. tryLock, 인터럽트 가능 lockInterruptibly, 공정성 옵션, 다중 Condition을 지원한다. 그러나 read와 write를 여전히 구분하지 않으므로 read-heavy 캐시 갱신용으로는 여전히 부적합하다.

ReentrantReadWriteLock

읽기 락과 쓰기 락을 분리한다.

  • 읽기 락은 여러 스레드가 동시에 보유 가능하다.
  • 쓰기 락은 단독 보유, 진입 시 모든 활성 read와 write가 끝나기를 기다린다.
  • write 보유 중이면 read도 막힌다.

HashMap 같은 공유 자료구조를 캐시로 두고 갱신할 때 가장 직관적인 도구다. 다만 읽기 락도 락이다. 매 read마다 락 객체의 내부 카운터를 CAS로 증가시키고 메모리 배리어가 발생한다. 초당 수만 read 환경에서는 이 비용이 무시 못 할 수준이 된다. 그리고 writer starvation 문제도 있다 — 끊임없이 읽기가 들어오면 쓰기가 영영 잡히지 않을 수 있어, 공정성 옵션을 켜면 throughput이 또 떨어진다.

StampedLock과 optimistic read

Java 8에서 도입된 StampedLock의 장점은 optimistic read다.

  • tryOptimisticRead()는 락을 잡지 않고 stamp(버전 번호)만 받는다. 비용이 거의 0에 가깝다.
  • 읽고 난 뒤 validate(stamp)로 그 사이에 쓰기가 있었는지 검증한다.
  • 검증 실패 시에만 정식 read lock을 잡고 다시 읽는다.

이 패턴은 "쓰기는 드물고 읽기는 빈번하다"라는 캐시 갱신 시나리오와 정확히 맞는다. write가 안 들어오는 99.9%의 경우, read는 락 없이 끝난다. write가 끼어들었을 때만 fall back한다.

단, StampedLock은 재진입을 지원하지 않고, Condition도 없으며, optimistic read 구간에서는 읽는 데이터가 일관된 상태가 아닐 수 있으므로 읽은 값을 일단 지역 변수로 복사한 뒤 validate로 검증해야 한다. 이 사용 규칙을 모르고 쓰면 오히려 위험하다.

volatile + AtomicReference: 통째 교체 패턴

캐시가 정적이고 일관된 스냅샷 단위로 갱신된다면, 락을 안 쓰고 끝낼 수도 있다.

  • 캐시 객체 자체를 volatile 또는 AtomicReference로 들고 있다가, 갱신 시 새로운 불변 캐시 객체를 통째로 만들어서 참조만 바꿔치기(swap)한다.
  • 조회 스레드는 락 없이 참조를 한 번 읽고, 그 시점의 스냅샷을 끝까지 사용한다.

이 패턴은 메뉴 캐시, 프로모션 정책 캐시처럼 "1~5분에 한 번 전체를 다시 빌드해도 되는" 경우에 가장 깔끔하다. 단점은 부분 갱신이 안 된다는 것 — 메뉴 한 줄을 바꾸려고 전체 캐시를 다시 만든다. 그러나 외식 커머스의 마스터 데이터는 보통 수백~수천 건 수준이라 전체 재빌드 비용이 크지 않다.

백엔드 실전 사용 — 메뉴/프로모션 캐시 갱신

후보자가 이전에 다뤘던 "슬롯 머신용 정적 데이터 캐시"는 사실상 같은 구조의 문제다. 게임 슬롯의 심볼 테이블/배당률은 운영자가 가끔 바꾸고, 게임 스레드는 매 스핀마다 읽는다. 외식 커머스로 옮기면 다음으로 매핑된다.

게임 도메인커머스 도메인
슬롯 심볼 테이블매장별 메뉴 마스터
배당률 테이블프로모션/할인율 정책
운영자 콘솔의 정적 데이터 변경점주/본사의 메뉴/가격/품절 변경
매 스핀의 배당 계산매 주문의 가격 계산

공통 패턴은 "운영 변경은 분 단위, 조회는 초 단위, 일관된 스냅샷이 필요" 라는 것이다. 따라서 채택할 수 있는 갱신 모델은 보통 다음 셋 중 하나다.

  1. 불변 객체 + AtomicReference swap — 점심/저녁 피크 직전 일괄 적용 가능, 부분 변경은 전체 재빌드.
  2. StampedLock optimistic read — 부분 변경이 잦고, 캐시 자료구조가 큰 경우.
  3. ReentrantReadWriteLock — 단순함을 우선시하고 read 빈도가 그렇게 극단적이지 않을 때.

bad vs improved 예제

예제 1 — synchronized로 메뉴 캐시를 막은 안티패턴

java
public class BadMenuCache {
    private final Map<Long, Menu> menus = new HashMap<>();
 
    public synchronized Menu get(long id) {
        return menus.get(id);
    }
 
    public synchronized void reload(List<Menu> latest) {
        menus.clear();
        for (Menu m : latest) menus.put(m.getId(), m);
    }
}

문제점:

  • 읽기끼리도 직렬화된다. 점심 피크에 초당 수천 건이 한 줄로 줄을 선다.
  • reload 중에는 clear() 직후의 빈 맵을 다른 스레드가 보지 못하긴 하지만, 어쨌든 모든 read가 멈춘다.
  • 메뉴 1건 변경에도 전체 reload를 호출하면 전체 트래픽이 일시 정지한다.

개선 1 — AtomicReference로 통째 교체

java
public final class MenuSnapshot {
    private final Map<Long, Menu> byId;
    public MenuSnapshot(Map<Long, Menu> byId) {
        this.byId = Map.copyOf(byId);
    }
    public Menu get(long id) { return byId.get(id); }
}
 
public class MenuCache {
    private final AtomicReference<MenuSnapshot> ref =
        new AtomicReference<>(new MenuSnapshot(Map.of()));
 
    public Menu get(long id) {
        return ref.get().get(id);
    }
 
    public void reload(List<Menu> latest) {
        Map<Long, Menu> next = new HashMap<>();
        for (Menu m : latest) next.put(m.getId(), m);
        ref.set(new MenuSnapshot(next));
    }
}
  • read는 사실상 락이 없다. 참조 한 번만 읽는다.
  • reload 중에도 read는 직전 스냅샷을 본다. tearing이 일어나지 않는다.
  • 부분 수정은 reload 한 번을 다시 호출해서 처리한다.

예제 2 — RWLock으로 조회 중 갱신 처리

부분 변경이 실제로 잦아서 매번 전체 reload가 부담스러울 때.

java
public class RwLockMenuCache {
    private final Map<Long, Menu> map = new HashMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
 
    public Menu get(long id) {
        lock.readLock().lock();
        try { return map.get(id); }
        finally { lock.readLock().unlock(); }
    }
 
    public void update(Menu m) {
        lock.writeLock().lock();
        try { map.put(m.getId(), m); }
        finally { lock.writeLock().unlock(); }
    }
}

read 동시성은 확보되지만 매 read마다 락 카운터를 만진다는 비용은 남는다.

개선 2 — StampedLock optimistic read

java
public class StampedMenuCache {
    private Map<Long, Menu> map = new HashMap<>();
    private final StampedLock sl = new StampedLock();
 
    public Menu get(long id) {
        long stamp = sl.tryOptimisticRead();
        Map<Long, Menu> snapshot = map;
        Menu found = snapshot.get(id);
        if (!sl.validate(stamp)) {
            stamp = sl.readLock();
            try {
                found = map.get(id);
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return found;
    }
 
    public void update(Menu m) {
        long stamp = sl.writeLock();
        try {
            Map<Long, Menu> next = new HashMap<>(map);
            next.put(m.getId(), m);
            map = next;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
}
  • 99% 이상의 read는 락 없이 끝난다.
  • write가 동시에 일어났을 때만 read 락으로 fall back.
  • write 시 새 Map을 통째로 만들어 swap하는 점이 핵심이다 — 기존 Map을 직접 put 하면 optimistic read 구간이 깨진 자료구조를 보게 되어 위험하다.

자주 만드는 실수 패턴

  • volatile Map 하나만 두고 그 Map에 put을 직접 한다 — Map 내부 상태가 깨진 채로 read 스레드에 보일 수 있다. swap 패턴이 아니면 안 된다.
  • StampedLock optimistic read 구간에서 객체의 두 필드를 따로 읽고 그대로 사용한다 — 두 필드가 서로 다른 시점일 수 있다. 지역 변수로 복사한 뒤 validate, 실패 시 read lock 재시도가 정석이다.
  • write lock 안에서 외부 호출(DB 조회, RPC)을 한다 — write 구간이 길어져 read 전체가 블록된다. write lock 진입 전에 새 데이터를 먼저 빌드해야 한다.
  • ReadWriteLock으로 starvation을 제어하지 않고 fair=false 그대로 운영 — write가 영영 안 잡히는 사고를 본다.

로컬 실습 환경

JDK 17+, Maven 또는 Gradle 단일 모듈로 충분하다. 외부 의존성 없이 JMH 또는 직접 짠 스레드 풀 벤치만으로 의미 있는 비교가 가능하다.

plaintext
src/main/java/cache/MenuCache.java        # AtomicReference 버전
src/main/java/cache/RwLockMenuCache.java
src/main/java/cache/StampedMenuCache.java
src/test/java/cache/CacheBench.java       # ExecutorService 기반 부하기

JMH를 도입할 수 있으면 @Benchmark 메서드 3종(read-only, read-heavy with 1% write, balanced)을 두고 비교한다. 도입하지 않더라도 Executors.newFixedThreadPool(64)에 read 스레드 32, write 스레드 1~2개를 섞어 30초 돌린 뒤 read 횟수를 비교하면 패턴별 throughput 차이가 명확히 보인다.

실행 가능한 미니 부하 테스트

java
public class CacheBench {
    public static void main(String[] args) throws Exception {
        StampedMenuCache cache = new StampedMenuCache();
        for (long i = 0; i < 1000; i++) cache.update(new Menu(i, "m" + i));
 
        AtomicLong reads = new AtomicLong();
        ExecutorService es = Executors.newFixedThreadPool(33);
        long end = System.currentTimeMillis() + 5000;
 
        for (int i = 0; i < 32; i++) {
            es.submit(() -> {
                ThreadLocalRandom r = ThreadLocalRandom.current();
                while (System.currentTimeMillis() < end) {
                    cache.get(r.nextLong(1000));
                    reads.incrementAndGet();
                }
            });
        }
        es.submit(() -> {
            while (System.currentTimeMillis() < end) {
                cache.update(new Menu(0, "updated"));
                Thread.sleep(50);
            }
            return null;
        });
 
        es.shutdown();
        es.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("reads=" + reads.get());
    }
}

StampedMenuCache, RwLockMenuCache, MenuCache(AtomicReference)를 차례로 끼워 넣고 reads 수치를 비교하면 read-heavy 시나리오에서 optimistic read와 swap 패턴의 우위가 가시화된다.

면접 답변 프레이밍

질문이 "동시성 어떻게 다루셨어요?" 또는 "캐시 갱신 중 조회 일관성은 어떻게 보장합니까?"로 들어오면, 다음 흐름이 안전하다.

  1. 상황 정의로 시작한다. "조회는 초당 수천, 변경은 분당 수 건 수준의 비대칭이라 read-heavy 정책 캐시 패턴으로 분류했습니다."
  2. 선택지 비교를 짧게 깐다. synchronized → 직렬화 비용, RWLock → read도 락 비용, StampedLock optimistic read → read 사실상 무비용, swap 패턴 → 부분 갱신 불가.
  3. 선택과 근거를 댄다. "데이터 양이 수천 건 수준이고 일관된 스냅샷 단위로 운영자가 적용하는 패턴이라 AtomicReference swap을 1차로 채택했습니다. 이후 부분 갱신 요건이 추가되어 StampedLock 기반으로 옮겼습니다."
  4. 이전 경험과 잇는다. "이전 직무의 게임 정적 데이터 캐시도 같은 구조였고, 운영자 콘솔 변경 시 새 스냅샷을 빌드해 통째 swap하는 방식으로 조회 latency 영향을 거의 없앴습니다. 외식 커머스 메뉴/프로모션 캐시도 같은 모델로 풀 수 있다고 봅니다."
  5. trade-off를 먼저 인정한다. "StampedLock은 재진입이 안 되고 사용 규칙이 까다로워서 팀 코드 리뷰 시 패턴을 가이드 문서로 고정해야 합니다."

이 흐름은 후보자가 단순히 키워드를 외운 것이 아니라 read/write 비대칭을 보고 도구를 고른다는 인상을 준다.

체크리스트

  • 캐시 read와 write의 빈도 비율을 숫자로 말할 수 있는가.
  • 부분 갱신이 진짜 필요한가, 전체 swap으로 충분한가를 판별했는가.
  • write 락 구간에 외부 호출(DB, 네트워크)이 들어가 있지 않은가.
  • StampedLock optimistic read 구간에서 읽은 값을 지역 변수로 복사했는가.
  • volatile Map 단독 사용으로 자료구조 내부를 직접 수정하는 코드가 없는가.
  • ReadWriteLock 사용 시 writer starvation 가능성을 검토했는가.
  • 갱신 중 조회 스레드가 보는 일관성 단위(스냅샷 vs 부분 갱신)를 문서로 합의했는가.
  • 운영 피크 직전 reload를 트리거하는 운영 절차/모니터링이 준비되어 있는가.
  • 면접 답변에서 도구 이름만이 아니라 read/write 비율과 trade-off로 설명할 수 있는가.
on this page
  • 01왜 지금 이 주제인가
  • 02핵심 개념 정리
  • thread safety의 두 축
  • synchronized
  • ReentrantLock
  • ReentrantReadWriteLock
  • StampedLock과 optimistic read
  • volatile + AtomicReference: 통째 교체 패턴
  • 03백엔드 실전 사용 — 메뉴/프로모션 캐시 갱신
  • 04bad vs improved 예제
  • 예제 1 — synchronized로 메뉴 캐시를 막은 안티패턴
  • 개선 1 — AtomicReference로 통째 교체
  • 예제 2 — RWLock으로 조회 중 갱신 처리
  • 개선 2 — StampedLock optimistic read
  • 자주 만드는 실수 패턴
  • 05로컬 실습 환경
  • 06실행 가능한 미니 부하 테스트
  • 07면접 답변 프레이밍
  • 08체크리스트

댓글 (0)