일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- junit 5
- 자바스터디
- System.in
- System.out
- github api
- 바운디드 타입
- 브릿지 메소드
- Switch Expressions
- 스파르타코딩클럽
- 항해99
- System.err
- 제네릭 타입
- 람다식
- 함수형 인터페이스
- yield
- 접근지시자
- 합병 정렬
- 프리미티브 타입
- 로컬 클래스
- annotation processor
- 상속
- 자바할래
- auto.create.topics.enable
- docker
- throwable
- raw 타입
- 정렬
- 익명 클래스
- 제네릭 와일드 카드
- Study Halle
- Today
- Total
코딩하는 털보
21.10.22 TIL 본문
Spring Data Redis
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
application.properties
spring.redis.host = localhost
spring.redis.port = 6379
Cacheable
Cache의 대상이 되는 정보들
- 단순한, 또는 단순한 구조의 정보들을 반복적으로 동일하게 제공해야 하거나
- 정보의 변경주기가 빈번하지 않고, 단위처리 시간이 오래걸리거나
- 최신화가 반드시 실시간으로 이뤄지지 않아도 서비스 품질에 영향을 거의 주지 않는 정보인 경우
Cache를 사용할때 주의해야 할 것
- 캐싱할 정보의 선택 -> 제일 중요하겠다.
- 캐싱할 정보의 유효기간 (TTL - Time To Live ) 설정
- 캐싱한 정보의 갱신시점
서비스를 설계할 때, 특히 백앤드의 경우 API 서비스의 기능설계단계에서 부터 Cache정책을 수립하는게 좋다
어떤 정보를 Cache로 적용할까를 먼저 따져보고 그 정보들을 어떤 시점에 어떤 주기로 갱신, 삭제를 할 지에 대한 최소한의 '캐싱전략'을 세우는 것도 어플리케이션 설계에서 중요한 영역중의 하나이다.
출처: https://yonguri.tistory.com/82 [대디장의 기억저장소]
Cacheable 사용하기
application.properties, 캐시 영역으로 redis를 사용하도록 설정
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
Application 클래스에 어노테이션 추가
@SpringBootApplication
@EnableCaching
public class ErrorPoolApplication {
}
@EnableCaching 어노테이션을 사용하면 Redis관련 Client가 자동으로 추가된다.
과거 버전의 스프링은 Jedis를 사용했지만 지금은 더 많은 장점을 가진 Lettuce를 채택하게 되었다.
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
Timestamped의 LocalDateTime을 역직렬화하려고 할 때 발생하는 에러.
캐싱에서 사용할 ObjectMapper 설정에 모듈을 추가해주면 해결할 수 있다.
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // timestamp 형식 안따르도록 설정
mapper.registerModules(new JavaTimeModule(), new Jdk8Module()); // LocalDateTime 매핑을 위해 모듈 활성화
return mapper;
}
완성된 Configuration 클래스
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration(redisHost, redisPort));
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.registerModules(new JavaTimeModule(), new Jdk8Module());
return mapper;
}
//이 Jackson2JsonRedisSerializer 설정은 Jackson2JsonRedisSerializer가 어플리케이션의 커스텀 클래스로 역직렬화 하지 못하는 문제로 추가했다.
//원인파악을 제대로 하려면 더 깊게 공부해야 한다.
//cannot deserialize value of type XXXXX from array value
private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = objectMapper();
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext
.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext
.SerializationPair
//RedisCacheConfiguration에 위에서 설정한 Jackson2JsonRedisSerializer를 사용
.fromSerializer(jackson2JsonRedisSerializer()));
Map<String, RedisCacheConfiguration> cacheConfiguration = new HashMap<>();
//key prefix, 캐시 만료시간을 설정할 수 있다.
cacheConfiguration.put("ARTICLE_LIST:", redisCacheConfiguration.entryTtl(Duration.ofSeconds(360L)));
return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
주요 어노테이션
@Cacheable
: 캐시에서 값을 가져온다. 만약 없으면 메서드 리턴을 캐시에 저장한다.@CacehPut
: 메서드 리턴을 캐시에 저장한다.@CacheEvict
: 캐시에서 값을 제거한다.
컨트롤러에서 Cacheable 어노테이션을 사용했다.
GET API의 요청에 있는 parameter, pathvariable 요소를 key 값으로 사용했다.
key값은 Spel로 작성하기 때문에 숫자 타입을 "#page+#skillId+#categoryId"
와 같이 작성하면 덧셈으로 계산된 결과가 key가 된다.
제대로 캐시 데이터들을 구분하려면 요청에 맞게 key가 선택되어야 하므로 "#page.toString()+#skillId.toString()+#categoryId.toString()"
로 변경하였다.
@ApiOperation(value = "항목 별 게시글 조회")
@GetMapping("/articles/skill/{skill_id}/{category_id}")
@Cacheable(value = "ARTICLE_LIST:", key = "#page.toString()+#skillId.toString()+#categoryId.toString()", cacheManager = "redisCacheManager")
public DefaultResponse<ArticlePageResponseDto> getArticlesInSkillAndCategory(
@ApiIgnore @AuthenticationPrincipal UserDetails userDetails,
@ApiParam(value = "페이지 번호", required = true) @RequestParam("page") Integer page,
@ApiParam(value = "주특기 번호", required = true) @PathVariable("skill_id") Integer skillId,
@ApiParam(value = "카테고리 번호", required = true) @PathVariable("category_id") Integer categoryId,
처음 요청을 할 때는 데이터베이스에서 데이터를 요청하는 것을 확인할 수 있다.
Hibernate: select distinct article0_.id as id1_0_0_, likes1_.id as id1_2_1_, article0_.created_at as created_2_0_0_, article0_.modified_at as modified3_0_0_, article0_.category as category4_0_0_, article0_.content as content5_0_0_, article0_.img_url as img_url6_0_0_, article0_.skill as skill7_0_0_, article0_.title as title8_0_0_, article0_.user_id as user_id10_0_0_, article0_.view_count as view_cou9_0_0_, likes1_.article_id as article_2_2_1_, likes1_.user_id as user_id3_2_1_, likes1_.article_id as article_2_2_0__, likes1_.id as id1_2_0__ from article article0_ left outer join like_info likes1_ on article0_.id=likes1_.article_id where article0_.skill=? and article0_.category=? order by article0_.created_at desc
Hibernate: select count(distinct article0_.id) as col_0_0_ from article article0_ where article0_.skill=? and article0_.category=?
Hibernate: select comments0_.article_id as article_5_1_2_, comments0_.id as id1_1_2_, comments0_.id as id1_1_1_, comments0_.created_at as created_2_1_1_, comments0_.modified_at as modified3_1_1_, comments0_.article_id as article_5_1_1_, comments0_.content as content4_1_1_, comments0_.user_id as user_id6_1_1_, user1_.id as id1_3_0_, user1_.email as email2_3_0_, user1_.password as password3_3_0_, user1_.role as role4_3_0_, user1_.skill as skill5_3_0_, user1_.social_id as social_i6_3_0_, user1_.username as username7_3_0_ from comment comments0_ inner join user user1_ on comments0_.user_id=user1_.id where comments0_.article_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: select user0_.id as id1_3_0_, user0_.email as email2_3_0_, user0_.password as password3_3_0_, user0_.role as role4_3_0_, user0_.skill as skill5_3_0_, user0_.social_id as social_i6_3_0_, user0_.username as username7_3_0_ from user user0_ where user0_.id=?
redis-cli로 어떤 key들이 있는지 확인할 수 있다. 위에서 나온 결과가 제대로 저장되어 있는 것을 확인했다. (key = 1, 1, 1)
[/Users/ijeong-in]> redis-cli
127.0.0.1:6379> keys *
1) "ARTICLE_LIST:::111"
이후로 서버에 동일한 GET 요청을 하면 데이터베이스에 쿼리가 발생하지 않는 것을 확인할 수 있을것이다.