이전에 학습한 레디스 캐시서버 를 실제 프로젝트에 도입하고 조회 API 를 날렸을때
1차 캐싱이 되어 조회성능이 빨라지는것을 테스트 해볼예정입니다.
프로젝스 설명 및 세팅
프로젝트의 구성은 스프링부트 , MySQL, Redis로 구성하였습니다.
Docker를 활용해서 Redis 및 MySQL을 구성하시는걸 추천드립니다
이유는 편하거든요 ㅎ
의존성 설치후
도커에서 레디스를 pull 하여 연결을 해줍시다.
이후 CMD에서 레디스가 잘 설치됐는지 ping을 날리면
pong이 옵니다.
이후
스프링부트에 캐시 매니저 설정과 레디스 캐시기반 데이터를 직렬화가 될수있게 설정을해줘야합니다.
import java.time.Duration;
@Configuration
@EnableCaching
public class CacheConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
// Hibernate5Module을 생성하고, 지연 로딩 프로퍼티를 강제로 초기화하도록 설정합니다.
Hibernate5Module hibernate5Module = new Hibernate5Module();
hibernate5Module.enable(Hibernate5Module.Feature.FORCE_LAZY_LOADING);
objectMapper.registerModule(hibernate5Module);
serializer.setObjectMapper(objectMapper);
return serializer;
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// GenericJackson2JsonRedisSerializer 사용
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
// 캐시의 기본 만료 시간 설정 등 추가 구성이 가능
;
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(config)
.build();
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 커스텀 ObjectMapper를 사용하여 타입 정보를 포함시킵니다.
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Hibernate5Module());
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
template.setValueSerializer(serializer);
template.setKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
그후 스프링부트에 설정을 Bean으로 등록합니다 (IoC에서 관리될수있게)
- **Jackson2JsonRedisSerializer**는 JSON 형태로 데이터를 직렬화하는 데 사용됩니다. **Hibernate5Module**을 사용하여 Hibernate 엔티티의 지연 로딩 문제를 처리하며, 여기서는 지연 로딩된 프로퍼티를 강제로 로딩하도록 설정합니다.
- cacheManager 메소드는 Spring의 캐싱 추상화를 위한 **CacheManager**를 구성합니다. 여기서는 **GenericJackson2JsonRedisSerializer**를 사용하여 값의 직렬화/역직렬화를 처리하며, 키는 **StringRedisSerializer**를 사용합니다. 이 설정은 Redis를 캐시 저장소로 사용할 때 적용됩니다.
- redisTemplate 메소드는 Redis 데이터 액세스 코드에서 사용할 **RedisTemplate**을 구성합니다. **RedisTemplate**는 Redis와의 상호 작용을 쉽게 만들어주는 고수준의 추상화를 제공합니다. 여기서는 직렬화 방식을 위해 커스텀 **ObjectMapper**를 사용하며, **GenericJackson2JsonRedisSerializer**로 값을 직렬화합니다. 이 설정을 통해 Java 객체를 Redis에 저장하고 조회할 때, 클래스 타입 정보를 유지할 수 있습니다.
일단 이렇게되면 레디스의 간단세팅은 끝입니다.
프로젝트에서 레디스 캐시를 도입할 API 설명
레디스의 캐시서버를 도입할 API를 설명드리겠습니다.
이런식으로 아티클의 내용과 페이징처리를 Response되는 api입니다.
아티클 서비스에 게시글 목록을 조회해보겠습니다.
{
"isSuccess": true,
"code": 200,
"message": "요청에 성공하였습니다.",
"result": {
"articles": [
{
"articleId": 1,
"title": "Content for article by user 1 number 0",
"contentPreview": "Content for article by user 1 number 0",
"authorName": "Test User 0",
"imageUrls": [
"https://example.com/image0.jpg"
]
.
.
중략
.
.
.
},
{
"articleId": 10,
"title": "Content for article by user 1 number 9",
"contentPreview": "Content for article by user 1 number 9",
"authorName": "Test User 0",
"imageUrls": [
"https://example.com/image9.jpg"
]
}
],
"currentPage": 0,
"totalItems": 10000,
"totalPages": 1000
}
}
이런식으로 게시물들의 내용과 현재 페이지 , 총 개수 , 토탈 페이지가 응답으로 오게됩니다.
도입전 아틸러리를 활용해 성능 분석
이제 아틸러리를 활용해 성능 분석을 들어가보겠습니다
첫 번째 단계:
- 지속 시간: 60초 (1분)
- 부하: 시작 시 초당 10명의 신규 가상 사용자가 시스템에 도착하며, 1분 동안 점차적으로 사용자 수가 초당 20명으로 증가합니다.
두 번째 단계:
- 지속 시간: 추가 120초 (2분)
- 부하: 이 단계에서는 초당 20명의 신규 가상 사용자가 지속적으로 시스템에 도착합니다.
시나리오 흐름:
- 각 가상 사용자는 0번 페이지의 게시글 목록을 조회한 후 1초 대기합니다.
- 이어서 1번 페이지의 게시글 목록을 조회하고, 다시 1초 대기합니다.
- 마지막으로 2번 페이지의 게시글 목록을 조회한 후, 다시 1초 대기합니다.
레디스 적용전 API 응답 시간
테스트는 총 3300개의 가상 사용자(VUs)를 생성하여 9900개의 HTTP 요청을 처리했습니다.
요약 통계:
- 총 생성된 가상 사용자: 3300명
- 총 HTTP 요청 수: 9900회
- 성공적인 응답: 9900회 (HTTP 200 코드)
- 평균 요청률: 59 요청/초
응답 시간 요약:
- 최소 응답 시간: 4ms
- 최대 응답 시간: 25ms
- 평균 응답 시간: 6.1ms
- 중위수 응답 시간 (p50): 6ms
- 75%의 응답 시간 (p75): 7ms
- 90%의 응답 시간 (p90): 7.9ms
- 95%의 응답 시간 (p95): 7.9ms
- 99%의 응답 시간 (p99): 10.9ms
- 99.9%의 응답 시간 (p999): 16.9ms
분석 및 해석 : 안정적인 응답을 유지하고있긴합니다. 이유는 로컬PC이기때문…. (감안해주세요 😂)
평균 응답 시간은 6.1ms , 최대 응답 시간은 25ms
도입후 아틸러리를 활용해 성능 분석
넵 이제 Config 설정과 레디스 실행까지 했으니
서비스레이어로 가보겠습니다.
@Cacheable(value = "articles", key = "'page:' + #page + ',size:' + #size")
위 어노테이션에 대해 설명 드리겠습니다.
추가적으로
삽입 , 수정 , 삭제시 **@CachePut**과 @CacheEvict 어노테이션을 사용하여 캐시를 업데이트하거나 무효화하는 데 사용합니다.
@CachePut
**@CachePut**은 메서드 실행 결과를 캐시에 업데이트할 때 사용합니다. 하지만, 여기서는 게시글 목록이 아닌 특정 게시글의 상세 정보를 캐시할 때 유용합니다. 만약 게시글의 상세 정보를 캐시하고 있다면, 게시글을 수정할 때 해당 캐시도 업데이트해야 합니다.
@CacheEvict
**@CacheEvict**은 캐시에서 항목을 제거할 때 사용합니다. 데이터가 변경될 때 (예: 삽입, 수정, 삭제) 관련된 캐시를 무효화하거나 삭제해야 할 때 유용합니다.
위 사진은 레디스에 저장된 직렬화된 데이터입니다
Jackson2JsonRedisSerializer를 통해 직렬화
자 같은 시나리오로 테스트를 돌려보겠습니다.
총 생성된 가상 사용자(VUs)와 HTTP 요청 수:
- 총 생성된 가상 사용자: 3,300명
- 총 HTTP 요청 수: 9,900회
성공적인 응답과 평균 요청률:
- 성공적인 응답: 9,900회 (HTTP 200 코드)
- 평균 요청률: 59 요청/초
응답 시간 요약:
- 최소 응답 시간: 0ms
- 최대 응답 시간: 1,043ms
- 평균 응답 시간: 3.6ms
- 중위수 응답 시간 (p50): 1ms
- 75%의 응답 시간 (p75): 2ms
- 90%의 응답 시간 (p90): 2ms
- 95%의 응답 시간 (p95): 3ms
- 99%의 응답 시간 (p99): 4ms
- 99.9%의 응답 시간 (p999): 572.6ms
분석 및 해석 : 응답 시간은 대부분 빠르며, 대다수의 요청이 1ms 내에 응답되었습니다. 그러나, p999(99.9%의 응답 시간) 값이 상대적으로 높아, 극소수의 요청이 더 긴 처리 시간을 가지는 것으로 보입니다. 아마 서버 부하 및 저의 로컬 PC의 리소스가 한계가 되서 느려진걸수도있습니다**( 도커 부터 해서 프로그램도 많이 돌리고있고 램도 용량이 작아요)**
테스트 총평
레디스를 사용하기 전과 후에 모든 백분위수(Percentile)가 개선되었습니다. 최소 응답 시간은 레디스 사용 후에 0ms로 개선되었고, 최대 응답 시간 역시 레디스 사용 전의 25ms에서 1,043ms로 증가했습니다. 이러한 변화로 인해 p999의 값이 이전보다 더 높아졌습니다.
결과적으로, 레디스를 사용함으로써 대부분의 요청의 응답 시간이 개선되었지만, 일부 요청의 응답 시간이 예상보다 더 길어진 것으로 보입니다.
단계 | 설명 | 기술 | 결과 |
문제 인식 | 캐시된 데이터 역직렬화 시 SerializationException과 JsonMappingException 발생. | 지연 로딩된 컬렉션이 초기화되지 않음 | |
원인 분석 | 지연 로딩된 속성의 역직렬화 문제 인식 | 예외 메시지 분석 | Hibernate 지연 로딩 문제 확인 |
설정 검토 | CacheConfig 설정 및 직렬화/역직렬화 메커니즘 검토 | CacheConfig 클래스 | 적절한 직렬화 방식 설정 필요성 인식 |
직렬화 조정 | ObjectMapper에 Hibernate5Module 등록하여 지연 로딩 속성 강제 초기화 설정 | Jackson2JsonRedisSerializer | 지연 로딩 속성의 강제 초기화 |
캐시 검증 | Redis CLI를 사용하여 캐시된 데이터의 키와 값 검증 | Redis CLI | 캐시 데이터 구조 및 저장 검증 |
해결 방안 구현 | - 커스텀 ObjectMapper 사용하여 Hibernate5Module 등록<br>- RedisCacheManager 및 RedisTemplate 설정 조정- 캐시 데이터 구조 재검토 및 조정 | Hibernate5Module, RedisTemplate | 캐시 문제 해결 및 성능 개선 |
이 API를 1차 캐시로 사용하지 말아야 하는 이유
저는 레디스를 경험해보고 응답속도가 빨라지는걸 눈으로 체험하기위해 이API를 썻지만
자주 변경되는 API라서 일관성 관리하는것이 어렵기도하고 캐시서버에 부하를 많이 줄수도있습니다. 여러분들은 적절한 로직을 찾아서 레디스를 구성해보세요 (그리고 캐시서버는 넘 비쌈 )
PS . 오픈소스 라이센스가 문제가 생겨 상업적으로 이용하지 못한다는 썰이있네요 .
1. 개인 사용자나 소규모 기업의 경우, 오픈소스 버전의 Redis를 계속 사용할 수 있기 때문에 큰 영향은 없을 것으로 보인다. 2. 대규모 서비스를 운영하는 기업이나 클라우드 서비스 제공업체의 경우에는, Redis와 상용 계약을 체결해야 하므로 비용 부담이 커질 수 있다. 따라서 일부 사용자는 다른 오픈소스 대안을 찾거나, Amazon 등의 포크 버전으로 이동할 가능성도 있어 보인다.
마무리 및 한줄평
레디스는 빠르다. 정말 공부 잘하고 써야겠다. 직렬화 ,,,, json ,,,, 😂