redis를 이용해서 스프링부트에 캐싱을 적용하는 법을 알아보겠습니다.
먼저 redis를 설치해야합니다. 도커를 이용해서 간단히 설치해보겠습니다.
$ docker run -d -p 6379:6379 redis
-p : redis 컨테이너의 6379포트를 외부에서 6379포트로 접근할 수 있게 합니다.
-d : 백그라운드로 컨테이너를 실행합니다.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3617c91d0177 redis "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:6379->6379/tcp nervous_khorana
컨테이너가 잘 올라왔습니다. 다음 명령어로 컨테이너에 접속해보겠습니다.
$ docker exec -it 3617c91d0177 /bin/bash
root@3617c91d0177:/data# redis-cli
127.0.0.1:6379> set kuke kyakya
OK
127.0.0.1:6379> get kuke
"kyakya"
127.0.0.1:6379>
exec 명령어로 쉘을 실행하여 컨테이너에 접속합니다.
redis-cli로 레디스에 접속할 수 있습니다.
스프링 부트에서 레디스를 이용하기 위해 build.gradle에 아래의 dependency를 추가해줍니다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
이제 스프링 설정파일에 redis 접속 호스트와 포트 번호를 적어줍니다.
#application.yml
spring:
redis:
host: 192.168.99.100
port: 6379
호스트는 컨테이너에 접근할 수 있도록 알맞게 적어주시면 됩니다.
이제 아래와 같이 RedisConfig 파일을 작성해줍니다.
@EnableCaching
@Configuration
public class RedisConfig {
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.host}")
private String host;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
@Bean
public CacheManager cacheManager() {
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.entryTtl(Duration.ofSeconds(CacheKey.DEFAULT_EXPIRE_SEC)) // default 만료 시간
.computePrefixWith(CacheKeyPrefix.simple())
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
// 캐시키별 default 유효기간 설정
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
cacheConfigurations.put(CacheKey.USER, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.USER_EXPIRE_SEC)));
cacheConfigurations.put(CacheKey.TICKET, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.TICKET_EXPIRE_SEC)));
cacheConfigurations.put(CacheKey.TICKETS, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.TICKET_EXPIRE_SEC)));
cacheConfigurations.put(CacheKey.REGION, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.REGION_EXPIRE_SEC)));
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory())
.cacheDefaults(configuration)
.withInitialCacheConfigurations(cacheConfigurations)
.build();
}
}
@EnableCaching 어노테이션을 작성하여 캐싱을 할 수 있도록 해줍니다.
위와 같이 cacheConfigurations에 각 키별로 default 만료 시간을 설정할 수 있습니다.
public class CacheKey {
public static final int DEFAULT_EXPIRE_SEC = 60; // 1 minutes
public static final String USER = "user";
public static final int USER_EXPIRE_SEC = 60 * 5; // 5 minutes
public static final String TICKET = "ticket";
public static final String TICKETS = "tickets";
public static final int TICKET_EXPIRE_SEC = 60 * 3; // 3 minutes
}
CacheKey는 직접 작성해준 것입니다.
이제 각 @Cacheable같은 어노테이션이나 위에서 등록한 RedisTemplate을 이용해서 레디스를 이용할 수 있습니다.
먼저 어노테이션을 이용한 사용 방법은 아래와 같습니다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MessageService {
@Cacheable(value = CacheKey.SENT_MESSAGES, key = "{#userId, #limit, #lastMessageId}")
public Slice<MessageDto> findSentMessagesByUserId(Long userId, Long lastMessageId, int limit) {
...
}
@Cacheable(value = CacheKey.RECEIVED_MESSAGES, key = "{#userId, #limit, #lastMessageId}")
public Slice<MessageDto> findReceivedMessagesByUserId(Long userId, Long lastMessageId, int limit) {
...
}
@Transactional
@Cacheable(value = CacheKey.MESSAGE, key = "#messageId")
public MessageDto readMessage(Long messageId) {
...
}
@Transactional
@Caching(evict = {
@CacheEvict(value = CacheKey.SENT_MESSAGES, key = "#requestDto.senderId", allEntries = true),
@CacheEvict(value = CacheKey.RECEIVED_MESSAGES, key = "#requestDto.receiverId", allEntries = true)
})
public MessageDto createMessage(MessageCreateRequestDto requestDto) {
...
}
@Transactional
public void deleteMessage(Long messageId) {
Message message = messageRepository.findById(messageId).orElseThrow(MessageNotFoundException::new);
cacheService.deleteMessagesCache(messageId, message.getSender().getId(), message.getReceiver().getId());
messageRepository.delete(message);
}
}
각 메소드 위에 캐시와 관련된 어노테이션을 작성해주었습니다.
하나씩 살펴보겠습니다.
@Cacheable(value = CacheKey.SENT_MESSAGES, key = "{#userId, #limit, #lastMessageId}")
public Slice<MessageDto> findSentMessagesByUserId(Long userId, Long lastMessageId, int limit) {
...
}
@Cacheable을 사용하면, 지정한 value와 key로 레디스의 key를 만들어줍니다.
기본적으로 CacheKey.SENT_MESSAGES::userId,limit,lastMessageId 와 같은 형태로 만들어줍니다.
예를 들어, CacheKey.SENT_MESSAGES == "smsg", userId == 3, limit == 15, lastMessageId == null 이라면,
"smsg::3,15,null"의 형태로 key가 만들어집니다.
해당 key의 value 값은, 메소드의 반환 값입니다.
@Transactional
@Caching(evict = {
@CacheEvict(value = CacheKey.SENT_MESSAGES, key = "#requestDto.senderId", allEntries = true),
@CacheEvict(value = CacheKey.RECEIVED_MESSAGES, key = "#requestDto.receiverId", allEntries = true)
})
public MessageDto createMessage(MessageCreateRequestDto requestDto) {
...
}
위와 같은 형태로 @Caching 어노테이션을 이용하면, 여러 개의 캐시 관련 이벤트를 처리할 수 있습니다.
@CacheEvict는 해당 value와 key값으로 만들어지는 캐시를 지워주는 어노테이션입니다.
key 생성 규칙은 위에서 설명한 @Cacheable과 동일합니다.
마지막에 allEntries = true로 열어주면, 해당 key의 접두사로 시작하는 모든 key를 제거해줍니다.
새로운 메시지가 작성되었기 때문에, 해당 송신자와 수신자로 인해 저장된 캐시를 모두 지워주는 과정이라고 보면 됩니다.
@Transactional
public void deleteMessage(Long messageId) {
Message message = messageRepository.findById(messageId).orElseThrow(MessageNotFoundException::new);
cacheService.deleteMessagesCache(messageId, message.getSender().getId(), message.getReceiver().getId());
messageRepository.delete(message);
}
위에서 보이는 메소드에서는, 별도의 캐시서비스를 생성하여 이용하였습니다.
그 이유는, 함수의 파라미터로 원하는 캐시 key에 접근할 수 없기 때문입니다.
메세지가 삭제되면, 해당 송수신자에 해당하는 메세지 캐시 내역을 지워줘야합니다.
작성된 CacheService에서의 deleteMessagesCache 메소드는 다음과 같습니다.
@Caching(evict = {
@CacheEvict(value = CacheKey.SENT_MESSAGES, key = "#senderId", allEntries = true),
@CacheEvict(value = CacheKey.RECEIVED_MESSAGES, key = "#receiverId", allEntries = true),
@CacheEvict(value = CacheKey.MESSAGE, key = "#messageId")
})
public void deleteMessagesCache(Long messageId, Long senderId, Long receiverId) {
...
}
위처럼 여러 개의 key를 이용해서 캐시를 삭제해주었습니다.
이외에도 @CachePut과 같이 업데이트가 일어나면, 캐시된 내역을 업데이트해주는 어노테이션도 있습니다.
다음으로 RedisTemplate을 이용해보겠습니다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class SignService {
private final JwtTokenProvider jwtTokenProvider;
private final RedisTemplate redisTemplate;
public void logoutUserToken(String token) {
redisTemplate.opsForValue().set(CacheKey.TOKEN + ":" + token, "v", jwtTokenProvider.getRemainingSeconds(token));
}
}
위와 같이 로그아웃을 구현할 때, 토큰을 캐시에 가지고 있으면 로그아웃 처리된 것으로 하였습니다.(예시)
사용법은 위와 같습니다.
redisTemplate에서 지원하는 메소드를 이용해서 캐시를 등록할 수 있습니다.
redisTemplate.opsForSet().add(...);
redisTemplate.opsForHash().put(...);
String 형태의 .opsForValue()뿐만 아니라, 다른 형태의 자료 구조도 redisTemplate을 통해 사용할 수 있습니다.
테스트를 위해 서버를 띄우고 2번이 1번에게 메시지를 전송하는 상황을 만들어보았습니다.
select message0_.message_id as message_1_3_0_, user1_.user_id as user_id1_8_1_, user2_.user_id...
로그를 확인하면 위와 같이 select 쿼리가 나가있습니다.
아까 지정한 어노테이션의 key와 value 값으로 인해, "sent_messages::2,15,null"와 같은 형태로 키가 만들어질 것입니다.
127.0.0.1:6379> keys *
1) "user::2"
2) "kuke"
3) "sent_messages::2,15,null"
redis에서 "keys *" 로 모든 key를 조회해봅니다.
해당 key가 만들어져있고, 재실행해도 더이상 select 쿼리가 나가지 않습니다.
이어서 바로 메세지 전송을 한번 더 수행한 결과입니다. 캐시가 삭제되어있습니다.
127.0.0.1:6379> keys *
1) "user::2"
2) "kuke"
연습 겸 이렇게 적용해봤지만, 캐시는 자주 변경되지 않는 데이터에 사용하는게 좋을 듯 합니다.
'Spring' 카테고리의 다른 글
Docker 이용해서 Spring Boot, Redis, Mysql 배포하기 (0) | 2021.11.09 |
---|---|
스프링부트 웹소켓 stomp를 이용한 실시간 알림 구현 (0) | 2021.11.09 |
스프링부트 JPA 무한스크롤 구현 (0) | 2021.11.09 |
스프링부트 JPA querydsl 대댓글(계층형 댓글) 기능 구현 (5) | 2021.11.09 |
Querydsl and, or 연산이 적용된 동적 쿼리 페이징 처리 (0) | 2021.11.09 |