1. Redis docker image pull
$ docker pull redis # redis 이미지 받기
$ docker images # redis 이미지 확인
$ docker run -p 6379:6379 --name some-redis -d redis # redis 시작하기
$ docker ps # redis 실행 확인
2. 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
3. application.properties
#Redis
spring.redis.host= localhost
spring.redis.port= 6379
Redis Cache 활성화를 위한 @Annotation
- @EnableCaching
- SpringBoot에게 캐싱 기능이 필요하다고 전달
- SpringBoot Starter class에 적용
- @Cacheable
- DB에서 애플리케이션으로 데이터를 가져오고 Cache에 저장하는 데 사용
- DB에서 데이터를 가져오는 메서드에 적용
- @CachePut
- DB의 데이터 업데이트가 있을 때 Redis Cache에 데이터를 업데이트
- DB에서 PUT/PATCH와 같은 업데이트에서 사용
- @CacheEvict
- DB의 데이터 삭제가 있을 때 Redis Cache에 데이터를 삭제
- DB에서 DELETE와 같은 삭제에서 사용
4. RedisConfig 파일 생성
/backend/redis/RedisConfig
package web.backend.redis;
import org.springframework.cache.CacheManager;
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.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
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 RedisConfig {
@Bean
public CacheManager testCacheManager(RedisConnectionFactory cf) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofMinutes(3L));
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build();
}
}
매니저에 도메인을 Serialize 하게끔 설정해두었기에 도메인에 따로 Serializable를 import 안해줘도 된다.
다만 어노테이션에서 매니저를 호출해야 한다.
backend/WebConfig
@Import({AopConfig.class, RedisConfig.class})
5. LocalDateTime in Domain
Serialize를 위해 LocalDateTime 타입 필드는 설정을 따로 해줘야 한다.
backend/module/user/User
...
@CreatedDate
@Column(name="user_createdat", updatable = false)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime user_creadtedat;
@LastModifiedDate
@Column(name="user_updatedat")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime user_updatedat;
...
6. Service 적용
package web.backend.module.user;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import web.backend.module.user.repository.UserQueryRepository;
import web.backend.module.user.repository.UserSpringJpaRepository;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserService {
private final UserQueryRepository userQueryRepository;
private final UserSpringJpaRepository userSpringJPARepository;
@Cacheable(value = "User", cacheManager = "testCacheManager")
public List<User> findAll() {
return userSpringJPARepository.findAll();
}
@Cacheable(value = "User", key = "#id", cacheManager = "testCacheManager")
public User findByIndexId(Long id) {
return userSpringJPARepository.findById(id).get();
}
public String save(User user) {
userSpringJPARepository.save(user);
return "ok";
}
@CachePut(value = "Order", key = "#id", cacheManager = "testCacheManager")
public String update(Long id, User user) {
User userOne = userSpringJPARepository.findById(id).get();
userOne.changeUser(user.getUserId());
return "ok";
}
@CacheEvict(value = "Order", key = "#id", cacheManager = "testCacheManager")
public String delete(Long id) {
userSpringJPARepository.deleteById(id);
return "ok";
}
}
테스트
1. 데이터 추가
테스트를 위해 WebConfig에 어플리케이션 빌드 시 데이터를 추가하는 로직을 작성해준다.
package web.backend;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import web.backend.aop.AopConfig;
import web.backend.interceptor.LogInterceptor;
import web.backend.module.user.User;
import web.backend.module.user.repository.UserSpringJpaRepository;
import web.backend.redis.RedisConfig;
@Configuration
@Import({AopConfig.class, RedisConfig.class})
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns(
"/css/**", "/*.ico"
, "/error", "/error-page/**" //오류 페이지 경로
);
}
@Autowired
UserSpringJpaRepository userSpringJpaRepository;
@Bean
public void addUsers() {
for(int i = 0; i < 100; i++) {
User user = new User();
user.changeUser("member" + i);
userSpringJpaRepository.save(user);
}
}
}
2. Redis Docker Container
Spring Server를 키고 Redis Container에 접속한 후 요청을 보내서 로그를 확인해보자.
$ docker ps
$ docker exec -it some-redis /bin/bash
$ redis-cli monitor
postman으로 요청을 보내보자.
그러면 아래와 같이 캐시가 저장된 것을 확인할 수 있다.
postman에도 데이터가 잘 도착했다.
이번엔 key를 지정하고 다시 보내보자. ( id를 파라미터로 보내는 로직 )
역시 정상적으로 동작이 잘 되었다.
3. 성능 확인
- 처음 캐시가 사용되기 전 요청
- 두번 째 요청
다만,
아래와 같은 점을 고려해야 한다.
- 리스트를 처음에 캐싱한다.
- 리스트의 어떤 항목이 생성되거나 수정되거나 삭제된다.
- 캐시된 리스트의 데이터가 바뀌어야 하지만 그렇지 못한다.
캐시에 변경사항이 생길 경우 다른 캐시에 영향이 끼친다면 로직을 따로 만들어 주어야 한다. 당장에라도 적용하고 싶으면
findAll()의 @Cacheable 어노테이션은 제거한 상태로 쓰고 나중에 각 테이블의 연관관계를 고려해서 로직을 구성해주자.