Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] - 좋아요 순 여행기 목록 조회 페이징 캐싱 #631

Merged
merged 10 commits into from
Jan 6, 2025
17 changes: 13 additions & 4 deletions backend/src/main/java/kr/touroot/global/config/CacheConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.time.Duration;
import kr.touroot.global.util.PageDeserializer;
import kr.touroot.global.util.SortDeserializer;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Sort;
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.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@EnableCaching
@Configuration
public class CacheConfig {

private static final Duration CACHE_TTL = Duration.ofMinutes(30);

@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager.builder(connectionFactory)
Expand All @@ -36,14 +41,18 @@ private RedisCacheConfiguration getRedisCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(keySerializationPair)
.serializeValuesWith(valueSerializationPair)
.entryTtl(Duration.ofHours(1L))
.entryTtl(CACHE_TTL)
.disableCachingNullValues();
}

private ObjectMapper getObjectMapperForRedisCacheManager() {
ObjectMapper objectMapper = new ObjectMapper();

objectMapper.registerModule(new JavaTimeModule());
SimpleModule pageModule = new SimpleModule();
pageModule.addDeserializer(PageImpl.class, new PageDeserializer());
pageModule.addDeserializer(Sort.class, new SortDeserializer());

objectMapper.registerModules(new JavaTimeModule(), pageModule);
objectMapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder().allowIfBaseType(Object.class).build(),
DefaultTyping.EVERYTHING
Expand Down
75 changes: 75 additions & 0 deletions backend/src/main/java/kr/touroot/global/util/PageDeserializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package kr.touroot.global.util;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;

public class PageDeserializer extends JsonDeserializer<PageImpl<?>> {

@Override
public PageImpl<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
List<Object> content = new ArrayList<>();
int pageNumber = 0;
int pageSize = 0;
long totalElements = 0;
Sort sort = Sort.unsorted();
Comment on lines +15 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

회의에서 말씀드린데로 저와 클로버는 Page를 캐싱했습니다.
그런데 캐싱된 페이지를 꺼내오는 과정에서 PageImpl로의 역직렬화가 실패했었는데요, 이는 PageImpl의 기본 생성자가 존재하지 않아 ObjectMapper가 리플렉션하지 못하기 때문이었습니다.

문제를 해결하기 위해서는 다음의 두가지 방법 중 하나를 선택해야 합니다.

  • PageImpl을 감싸는 Wrapper Class를 만든다.
  • PageImpl 객체 역직렬화를 커스터마이징

저와 클로버는 Wrapper 클래스를 운용하게 되는 경우 어색한 점이 있다고 판단했습니다.
Wrapper 클래스를 운용하게 되면 기존 코드의 수정이 불가피하고 캐싱하는 메서드와 캐싱하지 않는 메서드의 반환타입이 달라지는 부분이 클래스 운용 명세가 숨겨지는 잠재적 위험이라고 생각한 것이 이유에요.

따라서 PageImpl 객체를 커스텀 역직렬화하는 코드를 작성한 것이 해당 코드입니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 래퍼로 감싸서 해당 래퍼 클래스를 직접 사용하는 것은 좋지 못한 구조라 생각합니다.
캐시 도입이 어노테이션을 넘어서 동작하고 있는 서비스 코드에 변경을 주지 않는 것이 좋다고 생각합니다.

좋은 방향으로 역직렬화 문제를 해결해 주셨네요!


if (p.getCurrentToken() != JsonToken.START_OBJECT) {
throw new JsonParseException("Page 객체를 역직렬화하는 과정에서 예외가 발생했습니다.");
}

while (p.nextToken() != JsonToken.END_OBJECT) {
String fieldName = p.getCurrentName();
p.nextToken();

if (fieldName == null) {
continue;
}

if (fieldName.equals("content") && p.getCurrentToken() == JsonToken.START_ARRAY) {
content = ctxt.readValue(
p,
ctxt.getTypeFactory().constructCollectionType(List.class, Object.class)
);
continue;
}

if (fieldName.equals("number")) {
pageNumber = p.getIntValue();
continue;
}

if (fieldName.equals("size")) {
pageSize = p.getIntValue();
continue;
}

if (fieldName.equals("totalElements")) {
totalElements = p.getLongValue();
continue;
}

if (fieldName.equals("sort")) {
while (p.getCurrentToken() == JsonToken.START_OBJECT) {
p.nextToken();
}
sort = ctxt.readValue(p, Sort.class);
continue;
}

p.skipChildren();
}

PageRequest pageable = PageRequest.of(pageNumber, pageSize, sort);
return new PageImpl<>(content, pageable, totalElements);
}
}

77 changes: 77 additions & 0 deletions backend/src/main/java/kr/touroot/global/util/SortDeserializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package kr.touroot.global.util;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;

public class SortDeserializer extends JsonDeserializer<Sort> {

@Override
public Sort deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
List<Sort.Order> orders = new ArrayList<>();
boolean sorted = false;

Comment on lines +15 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 역직렬화 설정은 PageImpl을 생성하기 위해 필요한 Sort 객체의 역직렬화 로직입니다.

if (p.getCurrentToken() != JsonToken.START_OBJECT) {
throw new JsonParseException("Sort 객체를 역직렬화하는 과정에서 예외가 발생했습니다.");
}

while (p.nextToken() != JsonToken.END_OBJECT) {
String fieldName = p.getCurrentName();
p.nextToken();

if (fieldName.equals("sorted")) {
sorted = p.getBooleanValue();
continue;
}

if (fieldName.equals("orders")) {
deserializeOrders(p, orders);
continue;
}

p.skipChildren();
}

if (sorted) {
return Sort.by(Sort.Order.asc("dummy"));
}

return Sort.unsorted();
}

private void deserializeOrders(JsonParser p, List<Order> orders) throws IOException {
if (p.getCurrentToken() != JsonToken.START_ARRAY) {
throw new JsonParseException("Sort.orders 객체를 역직렬화하는 과정에서 예외가 발생했습니다.");
}

while (p.nextToken() != JsonToken.END_ARRAY) {
String direction = null;
String property = null;

while (p.nextToken() != JsonToken.END_OBJECT) {
String fieldName = p.getCurrentName();

if (fieldName.equals("property")) {
property = p.getText();
continue;
}

if (fieldName.equals("direction")) {
direction = p.getText();
}
}

if (property != null && direction != null) {
orders.add(new Order(Direction.fromString(direction), property));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import kr.touroot.travelogue.dto.response.TravelogueResponse;
import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -61,6 +62,11 @@ public TravelogueResponse findTravelogueByIdForAuthenticated(Long id, MemberAuth
return TravelogueResponse.of(travelogue, travelogueTags, likeFromAccessor);
}

@Cacheable(
cacheNames = "traveloguePage",
key = "#pageable",
condition = "#pageable.pageNumber <= 4 && #filterRequest.toFilterCondition().emptyCondition && #searchRequest.toSearchCondition().emptyCondition"
)
@Transactional(readOnly = true)
public Page<TravelogueSimpleResponse> findSimpleTravelogues(
TravelogueFilterRequest filterRequest,
Expand Down
Loading