Spring Boot + Redis는 환상의 조합이다. 정말 많은 개발자들이 사용하는 조합이고, Redis는 대규모 트래픽을 처리하는 서비스에서는 거의 필수요소라고 볼 수 있다. 왜냐? 아주 빠르고 캐싱이 가능하기 때문이다.
간단하게 캐싱 테스트를 해 볼 목적으로 Redis를 로컬에 설치해보자. (모든 설명은 macOS 기준으로 되어 있다)
Redis 설치
다음 링크에 아주 간단하고 자세하게 설치 방법이 나와있다.
먼저 다음 명령어로 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분정도로 설정했다.
'Spring Advanced' 카테고리의 다른 글
TestContainer를 이용한 Spring Boot 테스트 (2) | 2024.10.03 |
---|---|
Mockito를 사용한 스프링 프로젝트 단위 테스트 (4) | 2024.09.29 |
AOP (Part. 3) - 포인트컷 (0) | 2024.01.02 |
AOP (Part.2) (0) | 2024.01.02 |
AOP(Aspect Oriented Programming) (0) | 2023.12.29 |