반응형

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"

 

연습 겸 이렇게 적용해봤지만, 캐시는 자주 변경되지 않는 데이터에 사용하는게 좋을 듯 합니다.

반응형

+ Recent posts