[카프카활용] 챕터 6. Redis 사용
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나 메서드별로 잘 구분해서 캐시를 적용하는 것이 좋다.