diff --git a/backend/src/main/java/kr/touroot/global/config/CacheConfig.java b/backend/src/main/java/kr/touroot/global/config/CacheConfig.java index 72529099..b223ed70 100644 --- a/backend/src/main/java/kr/touroot/global/config/CacheConfig.java +++ b/backend/src/main/java/kr/touroot/global/config/CacheConfig.java @@ -3,12 +3,16 @@ 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; @@ -16,10 +20,11 @@ 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) @@ -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 diff --git a/backend/src/main/java/kr/touroot/global/util/PageDeserializer.java b/backend/src/main/java/kr/touroot/global/util/PageDeserializer.java new file mode 100644 index 00000000..09739a77 --- /dev/null +++ b/backend/src/main/java/kr/touroot/global/util/PageDeserializer.java @@ -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> { + + @Override + public PageImpl deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + List content = new ArrayList<>(); + int pageNumber = 0; + int pageSize = 0; + long totalElements = 0; + Sort sort = Sort.unsorted(); + + 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); + } +} + diff --git a/backend/src/main/java/kr/touroot/global/util/SortDeserializer.java b/backend/src/main/java/kr/touroot/global/util/SortDeserializer.java new file mode 100644 index 00000000..40aa9902 --- /dev/null +++ b/backend/src/main/java/kr/touroot/global/util/SortDeserializer.java @@ -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 { + + @Override + public Sort deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + List orders = new ArrayList<>(); + boolean sorted = false; + + 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 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)); + } + } + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java index cae32a9c..c68cbe3a 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java @@ -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; @@ -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 findSimpleTravelogues( TravelogueFilterRequest filterRequest,