본문 바로가기
프로젝트/Tripagram

Reids를 사용한 Cache 전략

by 히포파타마스 2023. 2. 6.

Redis를 사용한 Cache 전략

 

프로젝트 진행 중 개발을 하다 보니 자주 조회되지만 변하지 않는 값들이 보였다.

같은 값인데 조회될 때마다 RDB를 거쳐야 하는 것은 확실히 비효율적이다.

따라서 이런 값들을 캐싱해서 조회 시 RDB를 거치지 않게 하고 효율성을 높여보려고 하였다.

 

Spring에서는 메서드에 캐시를 적용할 수 있는 인터페이스를 제공하고 있으며, 이 프로젝트에서는 구현체로 Redis를 사용하였다.

Redis는 인메모리 기반의 비관계형 데이터베이스로 물리적인 디스크가 아닌 메모리에서 데이터를 처리하기 때문에 속도가 매우 빠르다.

이번 프로젝트에서는 설치가 간단하고, Spring에서도 손쉽게 사용할 수 있는 Redis를 선택하였다.

 

 

1. Redis 설치 & 사용법

 

[Ubuntu]

sudo apt-get install redis-server

 

[Mac]

brew install redis

 

다음 명령어로 설치된 redis의 정보를 볼 수 있다.

 

redis-server

 

[redis-server]

Redis가 실행되고 있는지, 사용되는 포트번호가 뭔지 확인할 때 사용하면 된다.

Redis에 Config파일을 만들어서 설정을 해줄 수도  있는데 나는 따로 Config파일을 만들지 않고 기본 설정을 사용하였다.

 

레디스에 접속하는 방법은 다음 명령어를 실행하면 된다.

 

redis-cli

 

[redis-cli]

 

 

set을 사용해서 데이터를 넣을 수 있다

 

set [key] [value]

 

[set]

 

 

get으로 데이터의 값을 찾아올 수 있다.

 

get [key]

 

[get]

 

  

특정 key를 검색할 수도 있다.

 

keys [keword]

[keys]

 

 

 

 

2. Spring Boot에 Redis 설정하기

이제 Spring에서 Redis를 설치하는 방법을 알아보자

 

2.1 @EnableCaching 어노테이션 설정

우선 스프링 프로젝트에서 캐시기능을 사용할 수 있도록 설정한다. 

 

[@EnableCaching]

@EnableCaching
@EnableJpaAuditing
@EnableScheduling
@SpringBootApplication
public class TravelRepoApplication {

    public static void main(String[] args) {
        SpringApplication.run(TravelRepoApplication.class, args);
    }

}

 

@EnableCaching을 설정하면 CachManager가 빈에 등록되고 프로젝트 내에서 Cache를 처리할 수 있는 어노테이션을 사용할 수 있게 된다

 

 

2.2 Redis 의존성 추가

Cache에 사용될 DB로 redis를 사용할 것이기 때문에 build.gradle에 Redis를 추가한다.

 

[Redis 의존성 추가]

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    .
    .
    .
}

 

 

2.3 Redis 설정정보 추가

사용되는 Redis에 대한 정보들을 설정파일에 추가한다.

 

[application.yml 설정 추가]

spring:
  redis:
    host: localhost
    port: 6379

 

서버 내부에 Redis를 설치했기 때문에 host는 localhost로, 포트번호는 Redis에 설정된 값(기본 6379)을 사용하면 된다.

이 프로젝트는 yml이 사용되었다.

 

 

2.4 Redis Config 설정

Spring에서 사용될 Redis에 대한 설정 클래스를 만든다.

 

[RedisConfig]

@Configuration
@RequiredArgsConstructor
public class RedisConfig {

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

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {

        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);

        return new LettuceConnectionFactory(redisStandaloneConfiguration);
    }
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);

        return redisTemplate;
    }
}

 

Spring에서 Redis를 사용하기 위한 설정들을 스프링 빈에 등록하는 설정 클래스이다.

 

 

[redisConnectionFactory]

@Bean
public RedisConnectionFactory redisConnectionFactory() {

    RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
    redisStandaloneConfiguration.setHostName(host);
    redisStandaloneConfiguration.setPort(port);

    return new LettuceConnectionFactory(redisStandaloneConfiguration);
}

 

Redis의 Connection을 생성하는 클래스를 빈으로 등록해 준다.

앞서 설정에 추가한 redis의 host와 port를 Redis 설정에 추가하고, 이를 구현체인 LettuceConnectionFactory에 적용해서 반환한다.

 

 

[redisTemplate]

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {

    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setKeySerializer(new StringRedisSerializer());    //[1]
    redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    redisTemplate.setConnectionFactory(connectionFactory);

    return redisTemplate;
}

 

redisTemplate는 프로젝트 내에서 redis를 직접 사용할 때 사용되는 클래스이다.

redisTemplate를 사용해서 java 코드로 redis에 값을 넣거나 조회할 수 있다.

직접 redis에 접근하지 않고 CacheManager의 어노테이션을 사용해서 Cache를 처리할 수 도 있지만 나중에 쓰기 지연 같은 다소 복잡한 처리를 해야 할 때는 Redis에 직접 접근해야 했기 때문에 redisTemplate를 설정해 주었다.

 

[1] : key와 value를 직렬화할 방법을 설정해 준다.

key는 StringRedisSerializer를 사용했고, Value에는 Api의 응답값이 들어갈 것이기 때문에 Json 형식을 직렬화하는 데 사용되는 GenericJackson2JsonRedisSerializer를 사용하였다.

마지막으로 Connection을 생성하는 ConnectionFactory를 설정해 준다.

 

 

2.5 CacheManager 등록

스프링에서 Cache를 처리할 수 있도록 지원해 주는 CacheManager를 Redis를 사용하는 구현체로 빈에 등록한다

 

[CacheConfig]

@Configuration
@RequiredArgsConstructor
public class CacheConfig {

    public final RedisConnectionFactory connectionFactory;

    @Bean
    public CacheManager redisCacheManager() {

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext
                        .SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext
                        .SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(connectionFactory)
                .cacheDefaults(redisCacheConfiguration).build();
    }
}

 

redisTemplate과 마찬가지로 key와 value에 직렬화 방식과 ConnectionFactory를 설정해 주면 된다.

 

 

 

 

3. Cach 적용

이제 CacheManager가 제공하는 어노테이션을 사용해서 메서드에 Cache를 적용해 보자

 

3.1 @Cacheable

사용자의 정보는 그리 쉽게 바뀌지 않지만 서비스 내에서 조회되는 빈도가 굉장히 많다.

따라서 사용자의 id를 받아서 사용자 정보를 반환하는 메서드에 Cache를 적용하였다.

 

@Cacheable을 메서드에 적용하면 해당 메서드가 호출될 때 그 반환값이 Caching 된다.

 

[@Cacheable]

@Cacheable(key = "#loginAccountId", value = "findLoginAccount")
public LoginAccountDetailsRes findLoginAccount(Long loginAccountId) {

    Account account = accountRepository.findById(loginAccountId)
            .orElseThrow(() -> new BusinessLogicException(ExceptionCode.NOT_FOUND_ACCOUNT));

    return LoginAccountDetailsRes.of(account);
}

 

Cache를 적용하고 싶은 메서드에 @Cacheable을 붙여주고 key와 value를 설정한다.

 

※ @Cacheable의 key와 value는 Redis의 key와 value가 아니라 Redis의 key를 구성하는 요소들이라고 생각하면 된다.

 

예를 들어, 위의 예제에서는 key를 #loginAccountId로, value를 findLoginAccount로 설정하였는데 해당 메서드가 실행되면 Redis에는 key가 다음과 같이 생성된다.

 

findLoginAccount::1

 

#을 붙이면 메서드의 파라미터 값을 사용할 수 있다.

만약 loginAccountId의 값이 1이라면 위와 같이 Redis에 key값이 저장된다.

 

Redis의 진짜 value에는 메서드의 반환값이 Json형식으로 저장된다.

 

@Cacheable이 적용된 메서드가 한번 실행됐다면 그 값이 Redis에 저장되고 다음 호출부터는 Redis에 저장된 값을 가져와서 반환한다.

 

그것이 Cache니까

 

@Cacheable을 적용한 후, 실제로 사용자 정보를 조회하는 API를 사용해 보자.

 

[Cache 적용 전]

 

첫 호출에는 Redis에 저장된 값이 없기 때문에 RDB에서 값을 조회해 온다.

312ms가 걸렸다.

 

[Reids]

 

사용자 정보를 한번 조회하면 설정해 둔 key값으로 반환값이 Redis에 저장된다.

 

[Cache 적용 후]

 

Redis에 값이 있을 때는 Redis의 값을 사용한다.

걸린 시간은 13ms로 RDB에서 값을 가져올 때에 비하면 매우 빨라진 것을 확인할 수 있다.

 

 

3.2 @CachEvict

앞서 사용자 정보를 반환하는 메서드에 Cache를 적용해 봤다.

그런데 만약 사용자 정보가 변경된 이후에도 갱신되지 않은 Redis에서 값을 가져오면 곤란할 것이다

 

때문에 사용자의 정보가 변경되는 메서드(수정, 삭제 등)가 호출되면  해당 Cache를 초기화해주어야 할 필요가 있다.

 

@CacheEvict를 사용하면, 해당 어노테이션이 붙은 메서드를 호출할 때 Cache데이터를 삭제(Redis의 특정 key를 삭제)할 수 있다.

@CacheEvict의 사용법은 @Cacheable과 같다.

 

[@CacheEvict]

@Transactional
@CacheEvict(key = "#loginAccountId", value = {"findAccount", "findLoginAccount"})
public IdDto modifyAccount(Long loginAccountId, AccountModifyReq accountModifyReq) {
	.
	.
	.
}

@Transactional
    @CacheEvict(key = "#loginAccountId", value = {"findAccount", "findLoginAccount"})
    public void removeAccount(Long loginAccountId) {
    .
    .
    .
}

 

회원 정보를 수정하거나 삭제할 경우 특정 Cache를 초기화하도록 하였다.

value에는 배열형식으로 복수의 값을 넣을 수 있다.

다만 @Cacheable를 사용할 때 Redis의 key를 key+value로 생성하도록 하면 key는 하나의 값만 사용할 수 있다.

 

 

3.3 @Caching

@CacheEvict를 사용할 때 key는 하나의 값만 넣을 수 있었다.

그러나 간혹 특정 메서드가 호출되었을 때 key가 다른 두 개의 값을 초기화해야 하는 경우가 있다.

 

예를 들어 이번 프로젝트에서는 Follow기능이 있었는데, Follow를 할 경우 Follow를 한 사용자와 Follow를 받은 사용자의 정보가 변하게 된다.

따라서 사용자의 정보에 대해 서로 다른 key를 가진 Cache를 초기화해야 했다.

 

@CacheEvict 어노테이션을 메서드에 두 개 붙이고 싶었지만 그럴 수는 없기 때문에 애를 먹었다.

다행히도 CacheManager는 Cache와 관련된 어노테이션을 여러 개 사용할 수 있는 @Caching이라는 어노테이션을 제공한다

 

[@Caching]

@Transactional
@Caching(evict = {
        @CacheEvict(key = "#loginAccountId", value = "findAccount"),
        @CacheEvict(key = "#accountId", value = "findAccount")
})
public FollowPostRes postFollow(Long loginAccountId, Long accountId) {
    .
    .
    .
}

 

@Caching을 사용해서 @CacheEvict를 두 번 적용하였고 서로 다른 key를 가진 cache를 초기화할 수 있었다.

 

 

이렇게 프로젝트에 Redis를 사용해서 Cache를 적용해 보았다.

언젠가 한번 해보고 싶었던 작업이었는데 이번 프로젝트에서 기회가 생겨 다뤄보게 되었다.

무조건 어려울 것이라고 생각했는데 막상 공부해 보니까 캐싱 자체는 CachManager를 통해서 어노테이션으로 쉽게 적용할 수 있었다.

그리고 데이터가 처리되는 구체적인 방식은 RDB를 사용하던 방식에서 인메모리 기반의 DB를 사용한다고 생각하니 이해하기 한결 쉬웠던 기억이 있다. 

 

CacheManager는 Cache를 조작하는 다양한 어노테이션들을 지원해 준다.

단순한 처리에는 제공되는 어노테이션들을 사용하면 되지만, 복잡한 처리방법이 요구된다면 직접 Redis에 접근해서 값을 처리해야 한다. 

나는 이번 프로젝트에서 조회수에 쓰기 지연을 적용할 때 redisTemplate를 사용해서 Redis에 직접 접근해서 값을 처리하는 방식을 사용하였다.

 

쓰기지연 전략에 관해서는 다음에 포스팅하겠다..!

'프로젝트 > Tripagram' 카테고리의 다른 글

쓰기지연 전략  (0) 2023.02.13
[설정] 초기 개발환경 구성  (0) 2023.01.20
Tripagram 회고  (0) 2023.01.02

댓글