Spring Boot Admin Server, Client config 설정하기

이미지
 Spring Boot로 많은 프로젝트를 진행하게 됩니다. 많은 모니터링 도구가 있지만, Spring Boot 어플리케이션을 쉽게 모니터링 할 수 있는 방법을 소개하려고 합니다.   코드 중심으로 살펴보겠습니다. 1. 어드민 서버 구축 1-1. 디펜던시 추가 dependencies { // https://mvnrepository.com/artifact/de.codecentric/spring-boot-admin-starter-server implementation 'de.codecentric:spring-boot-admin-starter-server:2.5.4' } 1-2. 어드민 서버 설정 활성화 @SpringBootApplication @EnableAdminServer public class ServerApplication { public static void main (String[] args) { SpringApplication. run (ServerApplication. class, args) ; } } EnableAdminServer를 하면 됩니다. 2. 클라이언트 서버 설정  예제는 book-client, member-client 2가지 클라이언트, member-client가 2개의 인스턴스 실행으로 작성했습니다.  2-1 디펜던시 추가 dependencies { // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web implementation 'org.springframework.boot:spring-boot-starter-web:2.5.4' // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-actuator implementation 'org.spring...

Spring boot redis cache config

   안녕하세요. RedisTemplate 에 이어 redis cache를 알아보겠습니다.  

전편 : Spring boot redis RedisTemplate에 대하여


  전편에서 한 Redis 설정이 되어 있다고 가정하겠습니다.



1. 의존성 추가

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

앞에서 의존성 추가를 이미 했으면 추가할 필요가 없습니다.


2. 캐시키 정의하기

import lombok.Getter;
import lombok.RequiredArgsConstructor;

import java.time.Duration;

@RequiredArgsConstructor
@Getter
public enum RedisCacheKey {
BOOK(CacheNames.BOOK, Duration.ofMinutes(2)),
STORE(CacheNames.STORE, Duration.ofHours(1)),
;
private final String cacheName;
private final Duration expired;

public static class CacheNames {
public static final String BOOK = "book";
public static final String STORE = "store";
}
}

Book과 Store 2가지 종류를 정의했습니다.

캐시키를 생성할 때 prefix로 사용될 cacheName과 만료시간(expired)를 선언했어요.


3. 캐시키 생성기 정의하기

import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;

public class RedisCacheKeyGenerator implements KeyGenerator {

public static final String DELIMITER = "_";

@Override
public Object generate(Object target, Method method, Object... params) {
return StringUtils.arrayToDelimitedString(params, DELIMITER);
}
}

  KeyGenerator은 정의하지 않아도, 캐시 기능은 문제없이 동작합니다. 하지만 운영하는데 있어, Redis에서 직접 데이터를 조회해야할 때 키를 만들때 다소 번거롭기 때문에 직접 정의해서 사용하면 도움이 됩니다.

  실제 키가 어떻게 형성 되는지는 뒤에서 살펴 보겠습니다.


3. 레디스 캐시 설정


import com.hevia.example.redis.RedisCacheKey;
import com.hevia.example.redis.RedisCacheKeyGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;

import java.util.HashMap;
import java.util.Map;

@Configuration
@RequiredArgsConstructor
@EnableCaching
public class RedisCacheConfig {

private final RedisConnectionFactory redisConnectionFactory;

@Bean
public CacheManager cacheManager() {
Map<String, RedisCacheConfiguration> cacheConfigurationMap = cacheConfigurations();
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.withInitialCacheConfigurations(cacheConfigurationMap)
.build();
}

private Map<String, RedisCacheConfiguration> cacheConfigurations() {
Map<String, RedisCacheConfiguration> cacheConfigurationMap = new HashMap<>();
for (RedisCacheKey each : RedisCacheKey.values()) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(each.getExpired());
cacheConfigurationMap.put(each.getCacheName(), redisCacheConfiguration);
}
return cacheConfigurationMap;
}

/**
* 커스텀 keyGenerator 생성
*
* @return
*/
@Bean
public KeyGenerator redisCacheKeyGenerator() {
return new RedisCacheKeyGenerator();
}
}

  정의된 RedisCacheKey를 기반으로 cacheManager 빈을 생성했습니다. (캐시 매니저를 2개 이상 사용하는 경우 빈의 이름을 다르게 지정할 수 있습니다.)

  위의 설정에서는 데이터가 어떻게 직렬화(serialize)가 되는지 정의하지 않고 기본을 사용했습니다. 이 경우 전편에서 본 JDK 직렬화로 저장이 됩니다.

redis cache key 설정에 기본직렬화 사용
  serializeValuesWith 지정 안했을 경우, 패키지 이동시 오류, 멤버 변수 추가/삭제시는 문제없습니다.


Book 클래스
import lombok.*;

import java.io.Serializable;
import java.time.LocalDateTime;

@AllArgsConstructor
@Getter
@Builder
@ToString
@Setter
@NoArgsConstructor
public class Book implements Serializable {

private static final long serialVersionUID = -5187004375976294105L;
private Integer id;
private String title;
private LocalDateTime createdDate;
}


java.lang.IllegalArgumentException: 
DefaultSerializer requires a Serializable payload but received an object of type
오류가 발생하면 Serializable을 구현해주면 됩니다.


import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
private Map<String, RedisCacheConfiguration> cacheConfigurations() {
Map<String, RedisCacheConfiguration> cacheConfigurationMap = new HashMap<>();
for (RedisCacheKey each : RedisCacheKey.values()) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(each.getExpired());
cacheConfigurationMap.put(each.getCacheName(), redisCacheConfiguration);
}
return cacheConfigurationMap;
}

  serializeValuesWith 에 GenericJackson2JsonRedisSerializer 설정을 추가했습니다. 

  • 패키지 이동 오류 발생
  • 멤버 변수 추가 오류 없음
  • 멤버 변수 삭제 오류 발생


  이번에는 저장은 json형태로 하고 정의한 클래스로 읽어보겠습니다.

  앞서 정의한 캐시키에 타입을 추가할게요.

BOOK(CacheNames.BOOK, Duration.ofMinutes(2), Book.class),
STORE(CacheNames.STORE, Duration.ofHours(1), Store.class),
;
private final String cacheName;
private final Duration expired;
private final JavaType javaType;

RedisCacheKey(String cacheName, Duration expired, Class clazz) {
this.cacheName = cacheName;
this.expired = expired;
this.javaType = TypeFactory.defaultInstance().constructType(clazz);
}


cacheConfigurations를 아래와 같이 수정하겠습니다.

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.hevia.example.redis.RedisCacheKey;
import com.hevia.example.redis.RedisCacheKeyGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.util.HashMap;
import java.util.Map;
public static final ObjectMapper REDIS_OBJECT_MAPPER;
static {
REDIS_OBJECT_MAPPER = new ObjectMapper();
REDIS_OBJECT_MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
REDIS_OBJECT_MAPPER.registerModule(new JavaTimeModule());
REDIS_OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}

private Map<String, RedisCacheConfiguration> cacheConfigurations() {
Map<String, RedisCacheConfiguration> cacheConfigurationMap = new HashMap<>();
for (RedisCacheKey each : RedisCacheKey.values()) {

Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(each.getJavaType());
jackson2JsonRedisSerializer.setObjectMapper(REDIS_OBJECT_MAPPER);

RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(each.getExpired());
cacheConfigurationMap.put(each.getCacheName(), redisCacheConfiguration);
}
return cacheConfigurationMap;
}

  위와같이 수정하게 되면 클래스 변경에 유연하게 대처가 가능합니다.


com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 
Java 8 date/time type `java.time.LocalDateTime` not supported by default:
add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
(through reference chain: com.hevia.example.book.Book["createdDate"])
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.*;

import java.time.LocalDateTime;

@AllArgsConstructor
@Getter
@Builder
@ToString
@Setter
@NoArgsConstructor
public class Book {

private Integer id;
private String title;
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createdDate;
}

JsonSerialize, JsonDeserialize를 추가하여 해결할 수 있습니다.


4. 저장된 레디스 캐시키 확인하기

  실제 키가 어떻게 생성되어 저장되는지 확인해보겠습니다.

  다음코드는 무의미한 코드로 cache key가 어떻게 생성되는지 확인하기 위해 작성했습니다.

@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK)
public Book no_param() {
return new Book();
}

@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK)
public Book param1(int id) {
return new Book();
}

@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK)
public Book param2(int id, String id2) {
return new Book();
}

캐시키 생성기는 기본을 사용하여 3가지, 파라미터가 없는경우, 1개인경우, 2개이상인경우 입니다.

캐시키 기본 생성기 키목록

:: 앞부분은 RedisCacheKey에 정의된 cacheName입니다. 뒷부분은 규칙이 다양해 보이는데 이것은 기본 생성기(SimpleKeyGenerator)를 확인하면 알수 있습니다.

public class SimpleKeyGenerator implements KeyGenerator {

@Override
public Object generate(Object target, Method method, Object... params) {
return generateKey(params);
}

/**
* Generate a key based on the specified parameters.
*/
public static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}

}

  코드를 보면 파라미터의 수에 따라 다르고 SimpleKey 객체를 사용하여 키를 생성하는것을 확인할 수 있습니다. 위의 내용을 파악하여 레디스에서 데이터를 조회해도 상관없지만 쉽게 사용하기 위해 직접 정의한 생성기를 사용해 보겠습니다.


@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK, keyGenerator = "redisCacheKeyGenerator")
public Book no_param() {
return new Book();
}

@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK, keyGenerator = "redisCacheKeyGenerator")
public Book param1(int id) {
return new Book();
}

@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK, keyGenerator = "redisCacheKeyGenerator")
public Book param2(int id, String id2) {
return new Book();
}
직접정의한 레디스캐시키 목록

  이전 보다는 보기 편해졌습니다.


  생성기(keyGenerator)를 사용하지 않고 직접 정의할 수도 있습니다.

@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK, 
key = "#book.id")
public Book get(Book book) {
return book;
}

@Cacheable(cacheNames = RedisCacheKey.CacheNames.BOOK,
key = "#book.id + '_' + #book.title")
public Book get2(Book book) {
return book;
}

  클래스가 파라미터로 전달되는경우 toString으로 키가 생성이 됩니다. 이보다는 key에 직접 정의하여 원하는 값으로 세팅할 수 있습니다.

직접 정의한 key 목록

5. @Cacheable과 aop

  cacheable 어노테이션은 aop로 동작하게 됩니다. 따라서 동일한 클래스에서 (내부함수)호출하는 경우 어노테이션이 동작하지 않습니다. 이것은 다른 어노테이션도 동일하니 사용하는데 주의가 필요합니다.








 



댓글

이 블로그의 인기 게시물

Spring boot redis RedisTemplate

MySQL PK 중복 (duplicate key ) 해결 방법