• 포스트
  • 도구
  • 작업
  • 포스트
  • 도구
  • 작업
로그인회원가입

© 2005 kaisa.co.kr. All Rights Reserved.

소개개인정보처리방침7083620@hanmail.net
← 목록으로
2026-05-01

캐시·Resilience·성능 최적화

캐시·Resilience·성능 최적화

요약: 측정 뒤에 캐시·재시도·서킷 브레이커·타임아웃을 넣고, 쿼리와 페이로드 병목을 줄인다.


1. 왜 측정이 먼저인가

감으로 캐시를 켜면 잘못된 키·TTL이 더 큰 장애가 된다. 순서는 측정 → 쿼리/N+1 → 풀·타임아웃 → 캐시 → 스케일 아웃이다. 가장 싼 최적화는 안 하는 일을 줄이는 것이다.


2. Spring Cache

한 줄 정의: 같은 입력의 비싼 조회 결과를 이름 붙은 저장소에 잠시 두는 것이다. @EnableCaching + @Cacheable / @CacheEvict가 선언적 API다.

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("products", "users");
    }
}
@Cacheable(cacheNames = "products", key = "#id")
public ProductResponse get(Long id) { }

@CacheEvict(cacheNames = "products", key = "#id")
public void update(Long id, UpdateProductRequest request) { }

ConcurrentMap은 프로세스 하나용이고 TTL이 없다. 단일 인스턴스에서 TTL이 필요하면 Caffeine이 기본선이다. 인스턴스가 여러 개면 Redis(spring-boot-starter-data-redis)다.

spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=10_000,expireAfterWrite=5m

캐시 키가 사용자와 무관하면 권한 데이터가 섞인다. TTL과 무효화를 같이 정한다. 캐시 스탬피드는 만료 순간 요청이 한꺼번에 원본을 두드리는 현상이다. 락·singleflight 또는 소프트 만료로 줄인다. @Cacheable 메서드가 self-invocation(같은 클래스 내부 호출)이면 프록시를 안 타서 캐시가 비어 있다.


3. Resilience4j

한 줄 정의: 원격 호출이 실패·지연할 때 재시도·차단·격리로 내 스레드와 의존성을 보호하는 라이브러리다. 아티팩트는 resilience4j-spring-boot3다.

외부 호출에는 타임아웃이 필수다. 타임아웃 없는 클라이언트는 가상 스레드든 플랫폼 스레드든 풀을 고갈시킨다.

JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory();
factory.setReadTimeout(Duration.ofSeconds(3));
RestClient client = RestClient.builder()
        .requestFactory(factory)
        .baseUrl(props.baseUrl())
        .build();

Retry

멱등하지 않은 POST에 무분별한 재시도는 결제를 두 번 만든다. 네트워크·408·429·5xx만, 그리고 Idempotency-Key가 있을 때만 재시도한다.

resilience4j:
  retry:
    instances:
      payment:
        maxAttempts: 3
        waitDuration: 200ms
        retryExceptions:
          - java.io.IOException
@Retry(name = "payment", fallbackMethod = "payFallback")
public PaymentResult pay(PaymentRequest request) {
    return paymentClient.charge(request);
}

Circuit Breaker

한 줄 정의: 실패율이 임계를 넘으면 호출을 잠시 끊고(open) 연쇄 실패를 막은 뒤, 시험 호출(half-open)로 닫는다.

resilience4j:
  circuitbreaker:
    instances:
      payment:
        slidingWindowSize: 20
        failureRateThreshold: 50
        waitDurationInOpenState: 10s

Bulkhead / RateLimiter / Fallback

Bulkhead는 동시 실행 수를 나눠 한 의존성이 스레드를 다 먹지 못하게 한다. Fallback은 비즈니스적으로 안전할 때만 쓴다. 가짜 성공은 재고·결제에서 금지다.

private PaymentResult payFallback(PaymentRequest request, Throwable t) {
    return PaymentResult.failed("temporary unavailable");
}

4. DB와 페이로드

  • WHERE / JOIN / ORDER BY에 인덱스, EXPLAIN ANALYZE로 확인
  • 목록은 엔티티 그래프 대신 DTO 프로젝션
  • 페이지 크기 상한, 장시간 트랜잭션 금지
spring:
  datasource:
    hikari:
      maximum-pool-size: 10
      connection-timeout: 3000

Hikari maximum-pool-size는 인스턴스 수 × 풀이 DB max_connections를 넘지 않게 둔다. 풀을 키우기 전에 슬로우 쿼리와 트랜잭션 길이를 본다.

오프셋 page=100000은 깊게 갈수록 느리다. 피드성 API는 키셋 페이지네이션—이미 본 마지막 id보다 작은 행만 가져오기—이 맞다.

List<Post> findByIdLessThanOrderByIdDesc(Long cursor, Pageable pageable);

큰 JSON은 전용 DTO와 @JsonIgnore로 줄이고, 파일은 오브젝트 스토리지 + CDN으로 뺀다.


5. 동시성과 멱등

재고 차감은 낙관적 락·DB 제약·단일 큐 중 하나를 고른다. Redis 분산 락은 만료·펜싱 토큰 없이 쓰기 쉽다.

@Entity
@Table(name = "idempotency_keys",
        uniqueConstraints = @UniqueConstraint(columnNames = "key_value"))
public class IdempotencyKey {
    @Id
    private String keyValue;
    private String responseHash;
}

컨테이너 메모리 한도에 heap을 맞춘다(MaxRAMPercentage). Native Image는 기동·RSS에 유리하지만 빌드와 리플렉션 제약이 있다.


6. 부하 테스트

k6·Gatling·JMeter로 같은 시나리오를 최적화 전후에 돌린다. RPS, p50/p95/p99, 에러율, CPU·힙·GC, DB 커넥션·slow query, 외부 의존성 지연을 같이 본다.


7. 흔한 실수

실수 대안
캐시만 넣고 evict 없음 갱신 경로에 @CacheEvict
같은 클래스 내부에서 @Cacheable 호출 별도 Bean으로 분리
사용자별 데이터를 공용 키 키에 주체 포함
비멱등 POST 재시도 키·안전한 상태 코드만
서킷 없이 재시도만 실패 폭풍 → 브레이커
타임아웃 0에 가까운 무한 대기 connect/read 명시

연습

  1. 상품 조회에 @Cacheable, 수정에 evict를 붙인다.
  2. 외부 호출에 CircuitBreaker + Retry를 적용한다.
  3. RestClient read timeout을 3초로 둔다.
  4. 느린 목록을 키셋 페이지네이션으로 바꾼다.

댓글

0

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.

이전 글비동기·메시징·Virtual Threads다음 글운영 배포 — Docker, 설정, Native Image, 프로덕션 체크리스트
← 목록으로