🍿 강의수강/카프카활용

[카프카활용] 챕터 6. Redis 사용

케로⸝⸝◜࿀◝ ⸝⸝ 2024. 5. 24. 10:21

Redis 개요

  • redis는 대출 관련 도메인보다 다른 도메인에서 많이 사용한다.
  • 사실 대출은 유저별로 개인화가 되어있고, 대출 요청을 여러 번 하는 경우가 별로 없기 때문에 캐시를 사용하더라도 캐시 히트율이 낮아서 큰 효율을 발휘하기는 어렵다.
  • 대출 상품들을 초기 화면에 쫙 뿌려줄 때는, 모든 유저가 동일한 화면을 보고 캐시 히트율도 높고 좋다.
  • 같은 데이터를 여러 번 조회했을 때 효율이 얼마나 잘 나오는지 테스트차 사용해 보자!
  • 같은 요청이 반복적으로 들어오는 API들은 이런 캐시처리를 해주는 것이 괜찮겠다-하고 다른 프로젝트할 때 참고하도록 하자.
  • 스프링에서 메모리를 잡아서 캐시를 제공(스프링 캐시) 하기도 한다.
    • 그러나 대부분의 서버는 서버를 한 대만 이용하지 않는다.
    • 그래서 공통적으로 사용하는 redis라는 솔루션을 사용하면 좋다.
    • 그래야 모든 서버들이 하나의 캐시를 바라보고 캐시 히트율이나, 메모리의 활용도도 서버의 개수에 비례에서 좋아진다.

 

PHP의 APC와 같은 역할을 하는 건가? 궁금해서 ChatGPT한테 물어보았다!

더보기

Q: PHP의 APC 캐시랑 스프링의 스프링 캐시는 비슷한 역할을 하는거야?

PHP의 APC (Alternative PHP Cache)

  • 역할: PHP의 Opcode 캐시입니다.
  • 기능:
  • 사용 목적: PHP 스크립트의 실행 성능을 향상시키기 위해 PHP 바이트코드를 캐싱.

 

스프링의 스프링 캐시 (Spring Cache)

  • 역할: 스프링 프레임워크의 애플리케이션 레벨 캐시입니다.
  • 기능:
  • 사용 목적: 애플리케이션의 데이터 조회 성능을 향상시키기 위해 데이터와 결과를 캐싱.

 

비교 및 요약

  • APC 캐시는 주로 PHP 코드의 바이트코드를 캐싱하여 PHP 스크립트의 실행 속도를 높이는 데 중점을 둡니다.
  • 스프링 캐시는 주로 애플리케이션의 데이터와 메서드 호출 결과를 캐싱하여 데이터 접근 속도를 높이고, 데이터베이스나 외부 API 호출 등의 반복적인 작업을 줄이는 데 중점을 둡니다.

 

따라서, 캐시 시스템은 각각의 환경과 목적에 맞게 설계된 다른 종류의 캐시라고 있습니다. PHP APC PHP 코드 실행을 최적화하는 반면, 스프링 캐시는 애플리케이션 데이터 접근을 최적화하는 사용됩니다.

 

Q: 그러면 APCu랑 스프링 캐시를 비교해줘.

PHP의 APCu와 스프링의 스프링 캐시를 비교하면 다음과 같은 차이점과 공통점을 발견할 수 있습니다:

APCu (APC User Cache)

  • 역할: PHP의 사용자 데이터 캐시
  • 기능:
  • 사용 목적: 데이터베이스 쿼리 결과, 세션 정보, 설정값 등 자주 사용되는 데이터를 캐싱하여 데이터베이스 부하를 줄이고 응답 시간을 개선.

 

스프링 캐시 (Spring Cache)

  • 역할: 스프링 프레임워크의 애플리케이션 레벨 캐시
  • 기능:
  • 사용 목적: 데이터 접근 성능을 향상시키고, 동일한 메서드 호출의 반복을 방지하여 응답 시간을 개선.

 

비교 및 요약

특징 APCu 스프링 캐시
기능 사용자 정의 데이터 캐싱 메서드 호출 결과와 데이터 캐싱
구현 방식 PHP 함수 (apcu_store, apcu_fetch ) 애너테이션 기반 설정 (@Cacheable )
사용 사례 세션 데이터, 설정값, 데이터베이스 쿼리 결과 데이터베이스 쿼리 결과, 계산 결과
캐시 저장소 PHP 애플리케이션 내부 메모리 다양한 캐시 구현체 지원 (Ehcache, Redis )
구성의 용이성 간단한 - 저장소 스프링 프레임워크와의 통합 다양한 옵션 제공

 

공통점

  • 목적: 애플리케이션의 성능 최적화를 위해 데이터 캐싱.
  • 성능 향상: 데이터베이스 부하 감소와 응답 시간 개선을 목표로 함.

 

차이점

  • 환경: APCu는 PHP 환경에서 사용되고, 스프링 캐시는 Java/Spring 환경에서 사용됨.
  • 기능적 차이: APCu는 주로 단순한 키-값 형태의 데이터 캐싱에 중점을 두는 반면, 스프링 캐시는 복잡한 캐싱 전략다양한 캐시 저장소를 지원하여 더 광범위한 캐싱 요구사항을 다룸.

 

결론적으로, APCu 스프링 캐시는 각각의 언어와 프레임워크 특성에 맞게 설계된 데이터 캐시 솔루션으로, 사용 목적과 환경에 따라 적합하게 선택하여 사용하면 됩니다.

 

build.gradle.kts

api 모듈에 의존성 추가

// Redis
implementation("org.springframework.boot:spring-boot-starter-data-redis")

 

 

application-domian.yml

설정은 기본 포트로

spring:
  jpa:
    generate-ddl: false
    show-sql: true
    hibernate:
      ddl-auto: validate
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/fintech?characterEncoding=UTF-8&serverTimezone=Asia/Seoul
    username: fintech
    password: fintech
  redis:
    host: localhost
    port: 6379

 

RedisCacheConfig

이 설정을 통해 스프링 애플리케이션에서 Redis를 캐시 저장소로 사용하고, 키와 값을 직렬화하여 저장하며, 캐시 항목의 TTL(Time To Live)을 10분으로 설정하게 된다.

package happyprogfrog.api.config

import org.springframework.cache.CacheManager
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.cache.RedisCacheConfiguration
import org.springframework.data.redis.cache.RedisCacheManager
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.RedisSerializationContext
import org.springframework.data.redis.serializer.StringRedisSerializer
import java.time.Duration

@Configuration
@EnableCaching
class RedisCacheConfig {

    @Bean
    fun redisCacheManager(cacheFactory: RedisConnectionFactory): CacheManager {
        val redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer()))
            .entryTtl(Duration.ofMinutes(10))

        return RedisCacheManager
            .RedisCacheManagerBuilder
            .fromConnectionFactory(cacheFactory)
            .cacheDefaults(redisCacheConfig)
            .build()
    }
}

 

1. 클래스 선언 및 애노테이션

@Configuration
@EnableCaching
class RedisCacheConfig {
  • @Configuration
    • 이 클래스가 스프링의 설정 클래스임을 나타냅니다. 스프링 컨테이너가 이 클래스를 스캔하여 빈(Bean)을 정의하고 관리합니다.
  • @EnableCaching
    • 스프링의 캐싱 기능을 활성화합니다. 이를 통해 애플리케이션에서 캐시를 사용할 있습니다.

 

2. redisCacheManager 메서드

@Bean
fun redisCacheManager(cacheFactory: RedisConnectionFactory): CacheManager {
    val redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer()))
        .entryTtl(Duration.ofMinutes(10))

    return RedisCacheManager
        .RedisCacheManagerBuilder
        .fromConnectionFactory(cacheFactory)
        .cacheDefaults(redisCacheConfig)
        .build()
}
  • @Bean
    • 이 메서드가 스프링 컨테이너에 의해 관리되는 빈을 생성함을 나타냅니다. 이 경우 redisCacheManager라는 이름의 빈이 생성됩니다.
  • redisCacheManager(cacheFactory: RedisConnectionFactory)
    • RedisConnectionFactory 매개변수로 받아 CacheManager 반환하는 함수입니다.

 

3.  RedisCacheConfiguration 설정

val redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer()))
    .entryTtl(Duration.ofMinutes(10))
  • RedisCacheConfiguration.defaultCacheConfig()
    • 기본 캐시 설정을 가져옵니다.
  • .serializeKeysWith(...)
    • 캐시의 키를 직렬화하는 방식을 설정합니다. 여기서는 StringRedisSerializer를 사용하여 문자열로 직렬화합니다.
  • .serializeValuesWith(...)
    • 캐시의 값을 직렬화하는 방식을 설정합니다. 여기서는 GenericJackson2JsonRedisSerializer를 사용하여 JSON으로 직렬화합니다.
  • .entryTtl(Duration.ofMinutes(10))
    • 캐시 항목의 TTL(생존 시간) 10분으로 설정합니다.

 

4. RedisCacheManager 생성

return RedisCacheManager
    .RedisCacheManagerBuilder
    .fromConnectionFactory(cacheFactory)
    .cacheDefaults(redisCacheConfig)
    .build()
  • RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cacheFactory)
    • 제공된 RedisConnectionFactory를 사용하여 RedisCacheManagerBuilder를 생성합니다.
  • .cacheDefaults(redisCacheConfig)
    • 이전에 정의한 RedisCacheConfiguration을 기본 캐시 설정으로 사용합니다.
  • .build()
    • 최종적으로 CacheManager 빌드하여 반환합니다.

 

ApiApplication

@EnableCaching 애노테이션 추가

package happyprogfrog.api

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.ComponentScan

@SpringBootApplication
@EntityScan(basePackages = ["happyprogfrog.domain"])
@ComponentScan(basePackages = ["happyprogfrog"])
@EnableCaching
class ApiApplication

fun main(args: Array<String>) {
    runApplication<ApiApplication>(*args)
}

 

LoanReviewServiceImple

대출 심사 결과를 내려주는 부분에 캐시를 사용한다.

package happyprogfrog.api.loan.review

import happyprogfrog.api.exception.CustomErrorCode
import happyprogfrog.api.exception.CustomException
import happyprogfrog.domain.domain.LoanReview
import happyprogfrog.domain.repository.LoanReviewRepository
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service

@Service
class LoanReviewServiceImpl(
    private val loanReviewRepository: LoanReviewRepository
): LoanReviewService {
    override fun loanReviewMain(userKey: String): LoanReviewDto.LoanReviewResponseDto {
        return LoanReviewDto.LoanReviewResponseDto(
            userKey = userKey,
            loanResult = getLoanResult(userKey)?.toResponseDto()
                ?: throw CustomException(CustomErrorCode.RESULT_NOT_FOUND)
        )
    }

    @Cacheable(value = ["REVIEW"], key = "#userKey", cacheManager = "redisCacheManager")
    override fun getLoanResult(userKey: String) =
        loanReviewRepository.findByUserKey(userKey)

    private fun LoanReview.toResponseDto() =
        LoanReviewDto.LoanResult(
            userLimitAmount = this.loanLimitedAmount,
            userLoanInterest = this.loanInterest
        )
}

 

테스트

  • 다음과 같이 응답 속도가 빨라진 걸 확인할 수 있다. (333ms -> 46ms)
  • 이렇듯 성능적으로 이득을 볼 수 있음
  • 하지만, 캐시는 메모리를 잡아먹고 캐시 히트율이 낮으면 넣어도 성능 향상이 크지 않다.
  • 따라서, API나 메서드별로 잘 구분해서 캐시를 적용하는 것이 좋다.

 

반응형