이번 포스팅에서는 실제로 프로젝트에 Spring Cache
를 적용해보고 성능 테스트까지 진행하려고 합니다.
Spring Cache
에 대한 전반적인 지식이 필요하신 분은 아래 링크를 참고해주시면 좋을 것 같습니다.
먼저 Spring Cache를 사용하기 위해 설정이 필요합니다.
위의 코드와 같이 Spring Boot Stater Cache
를 의존성 주입받아야 합니다.
Spring Boot Stater Cache
는 CacheManager
를 사용하여 자주 사용되는 데이터를 캐시 메모리에 저장하여 빠른 검색을 가능하게 하는 기능을 제공합니다.
이번 프로젝트에서는 캐시 데이터 저장소로 Redis를 사용하여 캐싱을 적용해보기로 하였습니다.
위에서 언급했듯이 CacheManager
를 사용하기 위해서는 어떤 CacheManger
를 사용할지 설정해줘야 합니다.
Redis와 연동하기 위해 RedisCacheManager
을 스프링빈으로 등록해주었습니다.
@EnableCaching
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.password}")
private String password;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration();
redisConfiguration.setHostName(host);
redisConfiguration.setPort(port);
redisConfiguration.setPassword(password);
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisConfiguration);
return lettuceConnectionFactory;
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
@Bean
public CacheManager cacheManager() {
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class) // 모든 클래스 허용
.build();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.activateDefaultTyping(ptv,ObjectMapper.DefaultTyping.NON_FINAL);
GenericJackson2JsonRedisSerializer redisSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(5))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer));
Map<String, RedisCacheConfiguration> configurations = new HashMap<>();
configurations.put(ConstantPool.CacheName.POST, defaultConfig.entryTtl(Duration.ofDays(1)));
configurations.put(ConstantPool.CacheName.VIEWS, defaultConfig.entryTtl(Duration.ofDays(1)));
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory())
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(configurations)
.build();
}
}
현재 프로젝트에서 이미 다른 곳에서 Redis를 사용하고 있기 때문에 이미 설정된 RedisConfig 파일에 CacheManager
를 생성해주는 스프링빈 코드를 작성해주었습니다.
기존 Config에 @EnableCaching
를 붙여주었고 아래에 CacheManager
빈에 대한 설정을 추가해주었습니다.
스프링빈 코드에 대한 자세한 내용은 보시려면 아래 링크를 참고하시면 좋을 것 같습니다.
일반 조회 로직과 Spring Cache
조회 로직의 성능을 비교해보기 위해 메서드를 따로 만들어주었습니다.
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/api/posts")
public class PostController {
private final PostService postService;
/**
* 게시글 단일 조회
*/
@GetMapping("/{post-id}")
public ResponseEntity<ApiResponse<PostResponseDto>> getPostDetail(@PathVariable("post-id") Long postId) {
ApiResponse<PostResponseDto> postDetail = postService.getPostDetail(postId);
return ResponseEntity.ok(postDetail);
}
/**
* 게시글 단일 조회 (캐싱 적용)
*/
@GetMapping("/v2/{post-id}")
public ResponseEntity<ApiResponse<PostResponseDto>> getPostDetailV2(@PathVariable("post-id") Long postId) {
PostResponseDto postDetail = postService.getPostDetailV2(postId);
return ResponseEntity.ok(ApiResponse.successOf(postDetail));
}
@Service
@RequiredArgsConstructor
@Transactional
public class PostService {
private final MemberRepository memberRepository;
private final PostRepository postRepository;
private final FileService fileService;
private final PostDetailRepository postDetailRepository;
@Transactional(readOnly = true)
public ApiResponse<PostResponseDto> getPostDetail(Long postId) {
PostResponseDto postDetail = postRepository.getPostDetail(postId).orElseThrow(() -> new ApplicationException(ErrorCode.POST_NOT_FOUND));
return ApiResponse.successOf(postDetail);
}
@Transactional(readOnly = true)
@Cacheable(value = ConstantPool.CacheName.POST, key = "#postId")
public PostResponseDto getPostDetailV2(Long postId) {
PostResponseDto postDetail = postRepository.getPostDetail(postId).orElseThrow(() -> new ApplicationException(ErrorCode.POST_NOT_FOUND));
return postDetail;
}
PostService
의 getPostDetailV2
가 캐싱이 적용된 조회 로직입니다.
@Cacheable
어노테이션을 사용하여 캐시 메모리(Redis)에 파라미터로 들어온 postId에 해당하는 키의 값이 존재할 시 그 값을 캐싱해서 반환해주고 없을 시엔 실제 DB에 조회 쿼리를 보내 데이터를 읽어오고 결과 값을 postId에 해당하는 키에 값으로 저장하도록 로직이 돌아가게 됩니다.
@Cacheable(value = ConstantPool.CacheName.POST, key = "#postId")
이렇게 설정해주었고 ConstantPool.CacheName.POST
는 "post" 값이며 key에는 파라미터로 들어오는 postId가 추가되어 저장되게 됩니다. 아래의 그림과 같이 말이죠.
실제로 성능이 좋아지는지 확인해보기 위해 Jmeter
툴을 이용하여 성능 테스트를 진행해보았습니다. Jmeter
에 대해 궁금하신 분은 아래 링크를 참고하시면 좋을 것 같습니다.
사용자 100명이 5번씩 요청을 발생시키는 시나리오를 가정해보았습니다.
HTML 보고서 생성을 통해 성능 테스트 결과를 보도록 하겠습니다.
먼저 테스트 결과 출력을 설정해주어야 합니다.
리스너로 만들어 두었던 Summary Report에서 파일 선택을 클릭하고 결과를 출력할 경로와 파일명을 설정합니다. 파일명은 자유롭게 하되, 확장자를 csv로 변경해줍니다.
열기를 눌렀을 때 오류 팝업이 나오지만 괜찮습니다. 확인을 눌러주시고, 파일 이름에 경로가 잘 입력되었음을 확인하시면 됩니다.
이후 이전 테스트 이력을 제거하시고 테스트를 실행해주시면 됩니다.
테스트 실행이 끝나면 앞서 지정했던 경로에 테스트 결과 파일이 생성됩니다.
이제 해당 테스트 결과파일을 HTML 보고서로 생성해봅시다.
Generate HTML report
창에서 테스팅 결과 csv 파일과 jmeter.properties, 출력하고 싶은 directory 경로를 순서대로 입력하시고 Generate report
를 클릭해주시면 HTML 테스트 결과가 출력하고 싶은 directory 경로에 추가되신 것을 확인하실 수 있습니다.
평균 응답 시간에 대한 결과를 보시면 Spring Cache 사용 게시글 조회
로직이 일반 게시글 조회
로직보다 1/2 정도 작은 것으로 나오고 있습니다.
HTTP 요청 샘플러 별로 가장 빠른 응답부터, 가장 느린 응답까지 그래프로 시각화해준 결과입니다.
Spring Cache 사용 게시글 조회
요청은 가장 느린 요청과 가장 빠른 요청의 편차가 심하지 않고, 대략 10200 ms ~ 10500 ms를 유지하고 있습니다.
일반 게시글 조회 테스트
는 빠르면 12000 ms도 걸리지 않지만, 느리면 21000ms 까지도 걸리는 것을 볼 수 있습니다. 편차가 굉장히 심한 것을 볼 수 있습니다.
다음은 TPS를 비교해보았습니다. 원래의 그래프에서는 Spring Cache 조회와 일반 조회를 같이 비교해보았는데 동일한 시간대의 두 로직의 TPS가 정확하게 조회가 되지않아 따로 진행하였습니다.
Spring Cache 사용 게시글 조회
의 TPS는 시간이 지날수록 증가하는 반면 일반 게시글 조회
의 TPS는 어느 구간 증가하다가 다시 감소하는 것을 볼 수 있습니다. 각 로직의 최대 TPS는 4.7로 동일하나 일정하게 TPS가 증가하는 추세를 보인 Spring Cache 사용 게시글 조회
이 성능적으로 좋다는 것을 알 수 있었습니다.