문제
각 MSA의 인가 핵심 경로에서 사용하는 UserSnapshot 캐시(RedisUserSnapshotCacheAdapter)에 두 가지 부하 집중 위험이 있습니다.
- 캐시 미스 시 single-flight 부재: TTL(기본 300초) 만료나 콜드 상태에서 동일 keycloakSub에 대한 동시 요청이 모두 Redis 미스를 겪으면 전부 User Service를 호출합니다. 요청별 cache-aside만 있고 락/single-flight가 없습니다. (캐시 key가 keycloakSub 단위이므로 스탬피드는 '동일 사용자'의 동시 요청 폭에 비례합니다.)
- 부정(음수) 캐싱 부재: 존재하지 않는 keycloakSub(404 → null →
USER_SNAPSHOT_NOT_FOUND)는 캐시에 저장되지 않습니다. 따라서 잘못되거나 만료된 토큰으로 들어오는 반복 요청이 매 요청마다 User Service를 호출합니다.
인가는 @RequireRole이 붙은 모든 메서드 진입 시 실행되는 매 요청 핵심 경로입니다.
근거
검증으로 확인한 실제 파일/라인:
src/main/java/com/bbd/securitycore/application/service/GetCurrentUserSnapshotService.java:57-62 — loadUserSnapshotCachePort.findByKeycloakSub(...).orElseGet(() -> loadFromUserServiceAndCache(...)). 미스 시 동시성 제어 없이 즉시 폴백.
src/main/java/com/bbd/securitycore/application/service/GetCurrentUserSnapshotService.java:80-85 — loadFromUserServiceAndCache는 snapshot == null이면 save(...)(line 84-85) 도달 전에 USER_SNAPSHOT_NOT_FOUND 예외를 던짐(line 80-82). 즉 NOT_FOUND 결과는 절대 캐싱되지 않음.
src/main/java/com/bbd/securitycore/adapter/out/http/UserServiceSnapshotAdapter.java:45-47 — User Service 404를 null로 변환(부재 사용자).
src/main/java/com/bbd/securitycore/adapter/out/redis/RedisUserSnapshotCacheAdapter.java:60-70 — save는 정상 스냅샷만 TTL과 함께 저장. 락/setIfAbsent 등 없음.
src/main/java/com/bbd/securitycore/config/BbdSecurityProperties.java:82 — userSnapshotCacheTtlSeconds = 300 (기본 300초).
src/main/java/com/bbd/securitycore/adapter/in/aop/RoleAuthorizationAspect.java:47-57 — @Before(@RequireRole) 어드바이스가 매 요청 getCurrentUserSnapshot() 호출 → 핵심 경로 확인.
src/main 전체 grep 결과 lock / single-flight / setIfAbsent / setnx / redisson / 음수 캐싱 로직 없음.
제안
우선순위는 낮게 두되(과설계 회피를 위해 실제 부하 측정 후 적용 권장), 다음 순서를 권장합니다.
- NOT_FOUND 음수 캐싱(우선): 부재 사용자에 대해 짧은 TTL(예: 30초)의 sentinel/tombstone 값을 캐싱하여 무효 토큰의 반복 호출이 User Service로 새어 나가지 않도록 합니다. 권한 부여 지연을 피하기 위해 양수 TTL(300초)보다 짧게 둡니다.
- 필요 시 single-flight: keycloakSub 단위로 짧은 분산락(
SET key val NX PX) 또는 in-process per-key 락을 적용해 동일 사용자 동시 미스 시 한 번만 User Service를 호출하도록 완화합니다.
각 단계는 독립 적용 가능하며, 1번이 위험 대비 노력이 가장 낮습니다.
영향 범위
bbd-security-core를 의존하는 모든 MSA(sales, inventory, item, procurement 등)의 인가 핵심 경로에 영향.
- 동작 변경은 캐시 계층에 국한되며 인가 판단 로직은 그대로 유지됨. 단 음수 캐싱 도입 시 사용자 생성/복구가 음수 TTL 동안 반영되지 않으므로 TTL을 짧게 설정해야 함.
- 변경 파일 후보:
RedisUserSnapshotCacheAdapter.java, GetCurrentUserSnapshotService.java, BbdSecurityProperties.java(음수 TTL 설정 추가).
문제
각 MSA의 인가 핵심 경로에서 사용하는 UserSnapshot 캐시(
RedisUserSnapshotCacheAdapter)에 두 가지 부하 집중 위험이 있습니다.USER_SNAPSHOT_NOT_FOUND)는 캐시에 저장되지 않습니다. 따라서 잘못되거나 만료된 토큰으로 들어오는 반복 요청이 매 요청마다 User Service를 호출합니다.인가는
@RequireRole이 붙은 모든 메서드 진입 시 실행되는 매 요청 핵심 경로입니다.근거
검증으로 확인한 실제 파일/라인:
src/main/java/com/bbd/securitycore/application/service/GetCurrentUserSnapshotService.java:57-62—loadUserSnapshotCachePort.findByKeycloakSub(...).orElseGet(() -> loadFromUserServiceAndCache(...)). 미스 시 동시성 제어 없이 즉시 폴백.src/main/java/com/bbd/securitycore/application/service/GetCurrentUserSnapshotService.java:80-85—loadFromUserServiceAndCache는snapshot == null이면save(...)(line 84-85) 도달 전에USER_SNAPSHOT_NOT_FOUND예외를 던짐(line 80-82). 즉 NOT_FOUND 결과는 절대 캐싱되지 않음.src/main/java/com/bbd/securitycore/adapter/out/http/UserServiceSnapshotAdapter.java:45-47— User Service 404를null로 변환(부재 사용자).src/main/java/com/bbd/securitycore/adapter/out/redis/RedisUserSnapshotCacheAdapter.java:60-70—save는 정상 스냅샷만 TTL과 함께 저장. 락/setIfAbsent등 없음.src/main/java/com/bbd/securitycore/config/BbdSecurityProperties.java:82—userSnapshotCacheTtlSeconds = 300(기본 300초).src/main/java/com/bbd/securitycore/adapter/in/aop/RoleAuthorizationAspect.java:47-57—@Before(@RequireRole) 어드바이스가 매 요청getCurrentUserSnapshot()호출 → 핵심 경로 확인.src/main전체 grep 결과 lock / single-flight /setIfAbsent/ setnx / redisson / 음수 캐싱 로직 없음.제안
우선순위는 낮게 두되(과설계 회피를 위해 실제 부하 측정 후 적용 권장), 다음 순서를 권장합니다.
SET key val NX PX) 또는 in-process per-key 락을 적용해 동일 사용자 동시 미스 시 한 번만 User Service를 호출하도록 완화합니다.각 단계는 독립 적용 가능하며, 1번이 위험 대비 노력이 가장 낮습니다.
영향 범위
bbd-security-core를 의존하는 모든 MSA(sales, inventory, item, procurement 등)의 인가 핵심 경로에 영향.RedisUserSnapshotCacheAdapter.java,GetCurrentUserSnapshotService.java,BbdSecurityProperties.java(음수 TTL 설정 추가).