728x90
반응형
SMALL

Spring Boot + Redis는 환상의 조합이다. 정말 많은 개발자들이 사용하는 조합이고, Redis는 대규모 트래픽을 처리하는 서비스에서는 거의 필수요소라고 볼 수 있다. 왜냐? 아주 빠르고 캐싱이 가능하기 때문이다.

 

간단하게 캐싱 테스트를 해 볼 목적으로 Redis를 로컬에 설치해보자. (모든 설명은 macOS 기준으로 되어 있다)

 

Redis 설치

다음 링크에 아주 간단하고 자세하게 설치 방법이 나와있다.

 

Install Redis on macOS

Use Homebrew to install and start Redis on macOS

redis.io

 

먼저 다음 명령어로 redis를 설치한다.

brew install redis

 

설치가 다 됐으면, 다음 명령어로 redis를 서비스로 등록한다.

brew services start redis

 

그럼 앞으로 백그라운드로 이 redis가 실행될 것이다. 만약 이 서비스를 종료하고 싶으면 다음 명령어를 실행하면 된다.

brew services stop redis

 

이 서비스의 정보를 확인하는 방법은 다음과 같다.

brew services info redis

이 명령어를 치면 다음과 같이 나온다.

redis (homebrew.mxcl.redis)
Running: ✔
Loaded: ✔
Schedulable: ✘
User: choichiwon
PID: 3404

 

Spring Boot에서 redis 의존성 내려받기

build.gradle

dependencies {
    ...
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    ...
}
  • 이 한줄만 추가해주면 끝난다.

Spring Boot에서 redis 설정하기

우선, 여러 방법으로 설정을 할 수 있는데, application.yaml 파일에서도 redis 설정을 할 수 있다. 근데 이렇게 하지 않을 것이다. 왜냐하면, 우선 첫번째 이유로는, redis가 로컬에 설치된 게 아니라 다른 외부에 있는게 아니라면 추가적인 설정이 필요가 없다.

 

왜냐하면 redis 의존성을 추가해주면 기본 설정이 다음과 같기 때문이다.

application.yaml

spring:
  data:
    redis:
      host: localhost
      port: 6379
  cache:
    type: redis

 

두번째로는, 이후에 알아보겠지만 관련 설정을 이 외부 설정 파일 말고 자바 코드로 풀 것이다.

그래서, 자바 코드로 하나씩 풀어보자. 나는 일단, RedisConfig라는 클래스 하나를 만들것이다.

 

RedisConfig

package cwchoiit.dmaker.config.redis;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class RedisConfig {}
  • 빈 껍데기의 클래스이다. 우선은 @Configuration, @EnableCaching 이 두개의 애노테이션만 있어도 충분하다.

 

이제 캐시를 적용할 것이다. 실제로 캐시가 적용되길 원하는 메서드에 다음과 같이 애노테이션을 붙여준다.

@Cacheable("developer")
public DeveloperDetailDTO getDeveloperByMemberId(@NonNull String memberId) {
    log.info("[getDeveloperByMemberId] memberId = {}", memberId);
    return developerRepository.findByMemberId(memberId)
            .map(DeveloperDetailDTO::fromEntity)
            .orElseThrow(() -> new DMakerException(NO_DEVELOPER));
}
  • @Cacheable() 애노테이션을 붙여준다. 그러면, 이 메서드가 호출되면 캐시된 값이 있으면 그 값을 가져오게 된다. 저기서 "developer"key를 의미한다.
  • 이대로 끝나면 안된다. 왜냐하면, redis는 스프링 부트 외부에 있는 서비스이다. 그렇다는 것은 서비스와 서비스간 통신을 하려면 규약이 필요하다. 데이터가 전송될 때, 전송할 때 같은 포맷, 형식으로 데이터를 주고 받아야 한다. 그래서 가장 간단한 방법은 캐시하려는 데이터(여기서는 DeveloperDetailDTO가 된다) 객체가 Serializable을 구현하면 된다. 다음 코드처럼.

DeveloperDetailDTO

package cwchoiit.dmaker.dto;

import cwchoiit.dmaker.entity.Developer;
import cwchoiit.dmaker.type.DeveloperLevel;
import cwchoiit.dmaker.type.DeveloperType;
import cwchoiit.dmaker.type.StatusCode;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DeveloperDetailDTO implements Serializable {
    private DeveloperLevel developerLevel;
    private DeveloperType developerType;
    private String memberId;
    private Integer experienceYears;
    private String name;
    private Integer age;
    private StatusCode statusCode;

    public static DeveloperDetailDTO fromEntity(Developer developer) {
        return DeveloperDetailDTO.builder()
                .developerLevel(developer.getDeveloperLevel())
                .developerType(developer.getDeveloperType())
                .memberId(developer.getMemberId())
                .experienceYears(developer.getExperienceYears())
                .name(developer.getName())
                .age(developer.getAge())
                .statusCode(developer.getStatusCode())
                .build();
    }
}
  • implements Serializable을 선언한다. 이럼 끝이다.

테스트 해보기

실제로 API를 날려보자. 다음 사진을 보자.

  • 최초에는 캐시가 없기 때문에, 직접 데이터베이스에서 값을 가져오는 모습이 보인다. SELECT문이 실행됐다.

저 이후에 다시 한번 API를 날려보면, 다음과 같이 캐시데이터를 가져온다.

  • 아예 서비스의 memberId를 보여주는 로그조차 찍히지 않았다. 즉, 캐시 데이터를 그대로 반환한 것이다.

 

그리고 실제로 redis-cli로 확인을 해보면 잘 저장되어 있다.

 

그럼 실제로 저 데이터가 어떻게 저장되어 있나 확인해보자.

우리가 알아볼 수 없는 유니코드로 보여진다. 직렬화를 하기 위해 Serializable을 구현했는데, 이게 자바 Serialization이기 때문에 사람이 알아보기가 힘들다. 그래서 사람이 알아보기 좋은 포맷이 뭘까? 바로 JSON이다. JSON으로 직렬화할 수 있겠지? 당연히 있다! 해보자!

 

JSON으로 직렬화 방법 바꾸기

이제 자바의 Serialization이 아닌 JSON 형태로 직렬화하기 위해 아까 빈 껍데기로 만들어 두었던 RedisConfig를 사용할 차례다.

 

RedisConfig

package cwchoiit.dmaker.config.redis;

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.serializer.GenericJackson2JsonRedisSerializer;

import static org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair.fromSerializer;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }
}
  • RedisCacheConfiguration을 반환하는 빈을 등록한다.
  • serializeValuesWith() 메서드를 사용해서, key 말고 value에 대한 직렬화를 JSON으로 하도록 설정한다. (어차피 key는 문자열로 잘 직렬화 된 것을 이미 위에서 확인했기 때문에)

 

그리고 이렇게 직렬화 방식을 변경했으면, 다시 아까 DeveloperDetailDTO가 Serializable을 구현한 것을 지워줘야 한다. 이제는 자바 방식이 아니라 JSON 방식으로 수정했으니 당연히 지워줘야 한다.

DeveloperDetailDTO

package cwchoiit.dmaker.dto;

import cwchoiit.dmaker.entity.Developer;
import cwchoiit.dmaker.type.DeveloperLevel;
import cwchoiit.dmaker.type.DeveloperType;
import cwchoiit.dmaker.type.StatusCode;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DeveloperDetailDTO {
    private DeveloperLevel developerLevel;
    private DeveloperType developerType;
    private String memberId;
    private Integer experienceYears;
    private String name;
    private Integer age;
    private StatusCode statusCode;

    public static DeveloperDetailDTO fromEntity(Developer developer) {
        return DeveloperDetailDTO.builder()
                .developerLevel(developer.getDeveloperLevel())
                .developerType(developer.getDeveloperType())
                .memberId(developer.getMemberId())
                .experienceYears(developer.getExperienceYears())
                .name(developer.getName())
                .age(developer.getAge())
                .statusCode(developer.getStatusCode())
                .build();
    }
}
  • implements Serializable을 제거했다.

 

그리고, 아까 테스트 하면서 생긴 캐시는 다시 삭제하자. redis-cli에서 `flushall` 명령어를 실행하면 된다.

 

 

이제 다시 테스트 해보자.

  • 실행 결과를 보면, 첫번째 요청은 캐시가 없기 때문에 데이터베이스에서 조회해왔다.
  • 두번째 요청은 서비스의 로그조차 호출되지 않고 바로 캐시 데이터를 반환했다.

 

그리고 redis-cli로 확인해봐도 아주 잘 나오고 이제는 value값도 아주 잘 보인다.

 

 

RedisConfig 추가 설정하기

Prefix 설정

다만, 한가지 아쉬운 점이 있다. 캐시를 저장한 모습을 보면 이렇게 보인다.

  • :: 이게 두번 나온다. 그리고 Redis는 컨벤션을 : 하나를 사용하는 것을 말하고 있다. 그런데 스프링은 왜 두개를 붙이는지는 모른다. 그래도 이걸 바꿀순 있다.

이땐, 아까 RedisConfig 클래스에서 설정 정보를 추가해준다 다음처럼.

@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
    return RedisCacheConfiguration.defaultCacheConfig()
            .computePrefixWith(name -> name + ":")
            .serializeValuesWith(fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
  • computePrefixWith(name -> name + ":") → 이렇게 prefix를 설정한다.
  • 이 상태에서 다시 캐시를 만들어보면 다음과 같이 `:`이 하나만 나온다.

 

TTL 설정

이번엔 Time to Live 값 설정이다. 지금 상태로는 서버가 내려가기전 까진 캐시가 무한정 살아있기 때문에 대규모 트래픽이나 사용량이 높은 서비스라면 이 경우 메모리가 부족한 현상이 일어나게 될 것이다. 그래서 캐시의 유효시간을 설정해줘야 한다.

 

RedisConfig

@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
    return RedisCacheConfiguration.defaultCacheConfig()
            .computePrefixWith(name -> name + ":")
            .entryTtl(Duration.ofMinutes(10))
            .serializeValuesWith(fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
  • entryTtl(Duration.ofMinutes(10)) → 간단하게 10분정도로 설정했다. 

 

 

728x90
반응형
LIST

+ Recent posts