Redis 캐시로 사용하기

김종하·2023년 8월 3일
0

Spring

목록 보기
1/1

환경세팅

  1. docker redis 설치
docker run --name redis -p 6379:6379 -d redis:alpine
  1. docker MySQL 설치
 docker run --name mysql-container -e MYSQL_ROOT_PASSWORD=<password> -d -p 3306:3306 mysql:latest
  1. JDK 17, spring boot 3.1.2

  2. dependecies

    dependencies {
    	// spring
    	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    	implementation 'org.springframework.boot:spring-boot-starter-web'
    	implementation 'org.springframework.boot:spring-boot-starter-cache'
    
    	// db driver
    	implementation 'com.mysql:mysql-connector-j'
    
    	// etc
    	compileOnly 'org.projectlombok:lombok'
    	annotationProcessor 'org.projectlombok:lombok'
    
    	// test
    	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }

RedisCacheConfiguration 빈 등록

import org.springframework.beans.factory.annotation.Value;
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.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
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
public class RedisCacheConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(60)) // 캐시된 엔트리들의 만료 시간(TTL - Time to Live)을 설정
                .disableCachingNullValues() // null 값을 캐시로 저장하지 않도록 하는 옵션
                .serializeKeysWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) // 캐시의 키를 직렬화(serialize)하는 방식을 지정
                )
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()) // 캐시의 값(value)을 직렬화하는 방식을 지정
                );
    }
}

조회 시 캐싱 적용

import lombok.RequiredArgsConstructor;
import me.jaden.redisstudy.member.api.request.MemberJoin;
import me.jaden.redisstudy.member.api.response.MemberView;
import me.jaden.redisstudy.member.domain.Member;
import me.jaden.redisstudy.member.repository.MemberRepository;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class MemberService {

    private final MemberRepository memberRepository;

    public Long joinMember(MemberJoin memberJoin) {
        Member joinedMember = memberRepository.save(memberJoin.convertToMember());
        return joinedMember.getId();
    }

 	// 캐싱적용
    @Transactional(readOnly = true)
    @Cacheable(value = "memberView", key = "#memberId")
    public MemberView findById(Long memberId) {
        Member member = findMember(memberId);
        return MemberView.convertFromMember(member);
    }

    private Member findMember(Long memberId) {
        return memberRepository.findById(memberId)
                .orElseThrow(() -> new EmptyResultDataAccessException(memberId + "에 해당하는 회원이 존재하지 않습니다.", 1));
    }
}

테스트코드

import me.jaden.redisstudy.member.api.response.MemberView;
import me.jaden.redisstudy.member.domain.Member;
import me.jaden.redisstudy.member.repository.MemberRepository;
import me.jaden.redisstudy.member.service.MemberService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@SpringBootTest
public class CacheTest {
    @MockBean
    private MemberRepository memberRepository;

    @Autowired
    private MemberService memberService;

    @Autowired
    private CacheManager cacheManager;

    private static final Long MEMBER_ID = 1l;

    @AfterEach
    void removeCache() {
        Cache cache = cacheManager.getCache("memberView");
        if (cache != null) {
            cache.evict(MEMBER_ID);
        }
    }

    @Test
    void cacheTest() {
        Member member = Member.builder()
                .id(MEMBER_ID)
                .name("jaden")
                .age(29)
                .email("jaden@email.com")
                .build();

        given(memberRepository.findById(MEMBER_ID))
                .willReturn(Optional.of(member));

        MemberView cacheMiss = memberService.findById(MEMBER_ID);
        MemberView cacheHit = memberService.findById(MEMBER_ID);

        assertThat(cacheMiss).isEqualTo(MemberView.convertFromMember(member));
        assertThat(cacheHit).isEqualTo(MemberView.convertFromMember(member));

        verify(memberRepository, times(1)).findById(MEMBER_ID);
    }
}

깃헙

0개의 댓글