docker run --name redis -p 6379:6379 -d redis:alpine
docker run --name mysql-container -e MYSQL_ROOT_PASSWORD=<password> -d -p 3306:3306 mysql:latest
JDK 17, spring boot 3.1.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'
}
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);
}
}