[Spring] Spring Cache 실제 적용해보기

김강욱·2024년 5월 7일
0

Project-Evertrip

목록 보기
8/19
post-thumbnail

이번 포스팅에서는 실제로 프로젝트에 Spring Cache를 적용해보고 성능 테스트까지 진행하려고 합니다.

Spring Cache에 대한 전반적인 지식이 필요하신 분은 아래 링크를 참고해주시면 좋을 것 같습니다.

Spring Cache에 대해

😁 Spring Cache 적용

먼저 Spring Cache를 사용하기 위해 설정이 필요합니다.

1. build.gradle 설정

위의 코드와 같이 Spring Boot Stater Cache를 의존성 주입받아야 합니다.

Spring Boot Stater CacheCacheManager를 사용하여 자주 사용되는 데이터를 캐시 메모리에 저장하여 빠른 검색을 가능하게 하는 기능을 제공합니다.

이번 프로젝트에서는 캐시 데이터 저장소로 Redis를 사용하여 캐싱을 적용해보기로 하였습니다.


2. CacheManager config 설정하기

위에서 언급했듯이 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 빈에 대한 설정을 추가해주었습니다.

스프링빈 코드에 대한 자세한 내용은 보시려면 아래 링크를 참고하시면 좋을 것 같습니다.

LinkedHasmap cannot be cast to class DTO Object 예외 발생


3. 조회 로직에 캐싱 적용하기

일반 조회 로직과 Spring Cache 조회 로직의 성능을 비교해보기 위해 메서드를 따로 만들어주었습니다.

Controller 코드
@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 코드
@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가 추가되어 저장되게 됩니다. 아래의 그림과 같이 말이죠.


😁 성능 테스트 By Jmeter

실제로 성능이 좋아지는지 확인해보기 위해 Jmeter 툴을 이용하여 성능 테스트를 진행해보았습니다. Jmeter에 대해 궁금하신 분은 아래 링크를 참고하시면 좋을 것 같습니다.

[Spring] Apache JMeter 사용 방법

사용자 100명이 5번씩 요청을 발생시키는 시나리오를 가정해보았습니다.

HTML 생성 및 확인

HTML 보고서 생성을 통해 성능 테스트 결과를 보도록 하겠습니다.

먼저 테스트 결과 출력을 설정해주어야 합니다.

리스너로 만들어 두었던 Summary Report에서 파일 선택을 클릭하고 결과를 출력할 경로와 파일명을 설정합니다. 파일명은 자유롭게 하되, 확장자를 csv로 변경해줍니다.

열기를 눌렀을 때 오류 팝업이 나오지만 괜찮습니다. 확인을 눌러주시고, 파일 이름에 경로가 잘 입력되었음을 확인하시면 됩니다.


이후 이전 테스트 이력을 제거하시고 테스트를 실행해주시면 됩니다.

테스트 실행이 끝나면 앞서 지정했던 경로에 테스트 결과 파일이 생성됩니다.

이제 해당 테스트 결과파일을 HTML 보고서로 생성해봅시다.

Generate HTML report 창에서 테스팅 결과 csv 파일과 jmeter.properties, 출력하고 싶은 directory 경로를 순서대로 입력하시고 Generate report를 클릭해주시면 HTML 테스트 결과가 출력하고 싶은 directory 경로에 추가되신 것을 확인하실 수 있습니다.


HTML 보고서 살펴보기

HTML 보고서 오버뷰

평균 응답 시간에 대한 결과를 보시면 Spring Cache 사용 게시글 조회 로직이 일반 게시글 조회 로직보다 1/2 정도 작은 것으로 나오고 있습니다.

응답시간 결과

HTTP 요청 샘플러 별로 가장 빠른 응답부터, 가장 느린 응답까지 그래프로 시각화해준 결과입니다.

Spring Cache 사용 게시글 조회 요청은 가장 느린 요청과 가장 빠른 요청의 편차가 심하지 않고, 대략 10200 ms ~ 10500 ms를 유지하고 있습니다.

일반 게시글 조회 테스트는 빠르면 12000 ms도 걸리지 않지만, 느리면 21000ms 까지도 걸리는 것을 볼 수 있습니다. 편차가 굉장히 심한 것을 볼 수 있습니다.


TPS 비교
1. Spring Cache 사용 게시글 조회

2. 일반 게시글 조회

다음은 TPS를 비교해보았습니다. 원래의 그래프에서는 Spring Cache 조회와 일반 조회를 같이 비교해보았는데 동일한 시간대의 두 로직의 TPS가 정확하게 조회가 되지않아 따로 진행하였습니다.

Spring Cache 사용 게시글 조회의 TPS는 시간이 지날수록 증가하는 반면 일반 게시글 조회의 TPS는 어느 구간 증가하다가 다시 감소하는 것을 볼 수 있습니다. 각 로직의 최대 TPS는 4.7로 동일하나 일정하게 TPS가 증가하는 추세를 보인 Spring Cache 사용 게시글 조회이 성능적으로 좋다는 것을 알 수 있었습니다.


참고 자료
Apache JMeter를 이용한 부하 테스트 및 리포트 생성

profile
TO BE DEVELOPER

0개의 댓글

관련 채용 정보