본문 바로가기

나의 여정/이것저것

Spring Cache + Redis Cache

🌊 Spring Cache + Redis Cache

Spring Boot
Spring Cache
Redis

[!IMPORTANT]
모든 마크다운 파일은 직접 본인이 작성하였습니다.

📌 개요

Spring Boot Starter Cache와 Redis를 활용한 캐시 전략 구현 및 성능 테스트 결과를 공유하고,
나에 생각을 공유

🏗 아키텍처

오늘은 Spring Boot Starter Cache + Redis 를 이용한 캐시 전략을 알아보았다.

동작 프로세스

  1. Client가 API 서버에 데이터 요청
  2. Redis Server에서 캐시 확인
  3. Cache Hit 시 DB 접근 없이 즉시 응답

💡 캐시란?

자주 사용하는 데이터나 값을 미리 복사해 놓는 임시 저장소

캐시를 사용함으로써 얻을 수 있는 효율
자주 접근하는 데이터를 메모리에 임시 저장함으로써 데이터베이스 쿼리 부하를 줄이고 응답 시간을 크게 단축시키며, 웹 서버와 데이터베이스 서버의 자원 사용률을 최적화하여 트래픽 급증 시에도 안정적인 서비스를 제공할 수 있게 합니다.


이런 캐시전략을 수립하는것을 확인하기 위해 실습을 진행해보았다.

🛠 개발 환경

  • Language: Java 17
  • Framework: Spring Boot 3.0.6
  • Dependencies:
    • Spring Web
    • Spring Data JPA
    • Spring Data Redis
    • Spring Cache
  • Cache Server: Redis
  • 부하테스트: Vegeta (Open Source)

실습 조건

  1. 2개의 API 구현
    • Cache 전략 없는 DB Connection Fetch API
    • Cache 전략을 사용하는 Fetch API
  2. API별 Vegeta 부하테스트 진행
  3. API별 부하테스트 결과 비교

Spring Cache 사용 전략 과정

  • Spring Cache를 적절하게 커스텀하여 캐싱방식을 어노테이션 방식으로 추상화 진행
@Configuration // Spring 설정 클래스임을 나타냄
@EnableCaching // Spring의 캐시 기능을 활성화
public class CacheConfig {

  // 캐시 이름을 상수로 정의하여 재사용성과 유지보수성 향상
  public static final String CACHE1 = "cache1";
  public static final String CACHE2 = "cache2";

  // 캐시별 속성을 정의하는 내부 클래스
  @AllArgsConstructor // 모든 필드를 파라미터로 받는 생성자 자동 생성
  @Getter // getter 메서드 자동 생성
  public static class CacheProperty {
    private String name; // 캐시 이름
    private Integer ttl; // 캐시 만료 시간(초)
  }

  @Bean
  public RedisCacheManagerBuilderCustomizer redisCacheConfiguration() {

    // Jackson의 다형성 타입 검증기 설정
    // Object 클래스의 모든 하위 타입에 대한 직렬화/역직렬화 허용
    PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
        .allowIfSubType(Object.class)
        .build();

    // ObjectMapper 설정
    var objectMapper = new ObjectMapper()
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) // JSON에 알 수 없는 필드가 있어도 역직렬화 실패하지 않음
        .registerModule(new JavaTimeModule()) // Java 8의 LocalDateTime 등 날짜/시간 타입 지원
        .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL) // JSON에 타입 정보를 포함시켜 정확한 타입으로 역직렬화 가능하게 함
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // 날짜를 타임스탬프가 아닌 ISO-8601 형식으로 직렬화

    // 캐시별 속성 정의
    // CACHE1: 5분(300초) 유지
    // CACHE2: 30초 유지
    List<CacheProperty> cacheProperties = List.of(
        new CacheProperty(CACHE1, 300),
        new CacheProperty(CACHE2, 30));

    // Redis 캐시 설정을 커스터마이징하는 빌더 반환
    return (builder -> {
      cacheProperties.forEach(cache -> {
        builder.withCacheConfiguration(cache.getName(),
            RedisCacheConfiguration.defaultCacheConfig()
                .disableCachingNullValues() // null 값은 캐시하지 않음
                .serializeValuesWith( // 캐시 키는 문자열로 직렬화
                    RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith( // 캐시 값은 JSON으로 직렬화
                    RedisSerializationContext.SerializationPair
                    .fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)))
                .entryTtl(Duration.ofSeconds(cache.getTtl()))); // 캐시 엔트리의 만료 시간 설정
      });
    });
  }
}
  • Fetch API Service Logic 구현
  • 1번과정에서 설정한 Custom Cache Config 를 통해 캐시전략을 추상화 하였다.
  • Service 로직을 실행 하기 전에 AOP 기반으로 파라미터 ID를 통해 Redis Server에 Cache Check를 진행하게 된다.
  /**
   * @Cacheable 어노테이션을 사용한 캐시 구현
   * 
   * 1. 동작 방식:
   *    - 최초 호출 시: DB에서 데이터를 조회하고 Redis에 저장
   *    - 이후 호출 시: Redis에서 캐시된 데이터를 직접 반환 (DB 조회 없음)
   * 
   * 2. 설정 설명:
   *    - cacheNames = CacheConfig.CACHE1: 캐시 저장소 이름 지정
   *    - key = "'user:' + #id": 캐시 키 형식을 'user:{id}' 로 지정
   * 
   * 3. 장점:
   *    - 반복적인 DB 조회 감소로 성능 향상
   *    - 캐시 관련 보일러플레이트 코드 제거
   *    - AOP 기반으로 비즈니스 로직과 캐시 로직 분리
   * 
   * @param id 사용자 ID
   * @return 찾은 사용자 정보
   * @throws RuntimeException 사용자를 찾을 수 없는 경우
   */
  @Cacheable(cacheNames = CacheConfig.CACHE1, key = "'user:' + #id")
  public User getUser3(Long id) {
    return userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
  }

부하테스트 과정

  • Vegeta 설치
# homebrew를 통해 vegeta 설치
brew install vegeta
  • vegeta 를 통해 Fetch API를 각각 vegeta를 통해 부하테스트 진행

테스트 조건

  • 15초 동안 진행
  • 5000/1s 초당 5000 트래픽 발생
  • 100개 Thread 사용

부하테스트 전

  • 현재 아키텍쳐의 전부는 Docker Container로 실행되어 있기때문에
    Docker Stats 를 사용한 리소스를 감지하여 테스트 진행

왼쪽 : 부하테스트 터미널 , 오른쪽 : (위) Postgres 리소스 모니터링 , (아래) Redis Server 리소스 모니터링

부하테스트 후

  • 캐시전략을 수립하지 않은 Fetch API
    • 1번사진에서 API 요청별로 DB Connection 으로 인해 Postgres Server CPU가 과부화 됨을 알 수 있다.
    • 2번사진에서 Succes 가 89% 로 현저하게 떨어짐을 알 수 있다 ( 부화로 인한 I/O로 인해 모든 Request를 처리하지 못함)
  • 캐시전략을 수립한 Fetch API
    • 1번 사진을보아 알 수 있듯이 캐시전략을 사용해 DB는 사용하지 않고 , Redis만 리소스를 사용하는것으로 확인
    • 2번 사진에서는 DB보다 높은 성공률을 보여줌
    • 100%가 아닌 이유는 타임아웃 설정으로 인해
      초당 10,000개의 매우 높은 동시 요청으로 인해 서버가 일부 요청을 제한된 시간(30초) 내에 처리하지 못했기 때문이다.
      이런점은, 인프라 구조 또는 서버 리소스에 따라 Redis ( active,idle ) 설정을 해주는 것으로 해결 할 수 있다.


📝 후기

1. 캐시 전략의 효율적인 활용

✅ 좋은 사용 사례

  • 효율적인 리소스 관리 가능
  • 서비스 확장성 개선
  • 시스템 성능 최적화

⚠️ 주의해야 할 점

  • 무분별한 캐시 전략 남발 시 문제 발생
    • 데이터 정합성 이슈
    • 불필요한 메모리 낭비
    • 운영 관리 복잡도 증가

핵심: 성능 개선이 필요한 곳에 선별적으로 적용하는 것이 중요

2. Spring Cache 첫 도입 소감

기존 방식 vs Spring Cache

구분 RedisTemplate Spring Cache
구현방식 API 별 Cache 정책 로직 구현 필요 Config 설정으로 추상화
AOP 구현 별도 구현 필요 내장 지원
복잡도 높음 상대적으로 낮음

장점

  • 설정 기반의 간편한 캐시 관리
  • 효율적인 코드 작성 가능
  • 높은 추상화 수준

설정이 다소 복잡할 수 있으나, 추상화를 통한 효율적인 코드 작성이 가능한 점이 큰 장점

 

 

Good~

'나의 여정 > 이것저것' 카테고리의 다른 글

Redis Replication ( Cluster )  (1) 2024.12.08