Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions src/main/java/com/example/Jinus/config/RedisCacheManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,53 @@ public class RedisCacheManager {
public CacheManager contentCacheManager(RedisConnectionFactory cf) {
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();

// 1. 식당 리스트 캐시 (불변 데이터, 7일 유지)
// 1. 식당 리스트 캐시 (불변 데이터, 30일 유지)
cacheConfigurations.put("cafeteriaList",
RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // Key Serializer
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // Value Serializer
.entryTtl(Duration.ofDays(7)) // 7일 유지
.entryTtl(Duration.ofDays(30))
.disableCachingNullValues()); // null 캐싱 방지


// 2. 식단 데이터 캐시 (하루 단위로 갱신)
cacheConfigurations.put("dietList",
// cacheConfigurations.put("dietList",
// RedisCacheConfiguration.defaultCacheConfig()
// .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // Key Serializer
// .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // Value Serializer
// .entryTtl(Duration.ofHours(12))
// .disableCachingNullValues());

// 3. 캠퍼스 이름 캐시 (불변 데이터, 30일 유지)
cacheConfigurations.put("campusName",
RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // Key Serializer
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // Value Serializer
.entryTtl(Duration.ofHours(12)) // 12시간 유지
.disableCachingNullValues());
.entryTtl(Duration.ofDays(30))
.disableCachingNullValues()); // null 캐싱 방지

// 3. 캠퍼스 이름별 캠퍼스 id 캐시 (불변 데이터, 7일 유지)
cacheConfigurations.put("campusName",
// 3. 식당 id 캐시 (불변 데이터, 30일 유지)
cacheConfigurations.put("cafeteriaId",
RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // Key Serializer
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // Value Serializer
.entryTtl(Duration.ofDays(7)) // 7일 유지
.entryTtl(Duration.ofDays(30))
.disableCachingNullValues()); // null 캐싱 방지

// 3. 식당별 식당 id 캐시 (불변 데이터, 7일 유지)
cacheConfigurations.put("cafeteriaId",
// 4. 캠퍼스 id 캐시 (불변 데이터, 30일 유지)
cacheConfigurations.put("campusId",
RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // Key Serializer
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // Value Serializer
.entryTtl(Duration.ofDays(7)) // 7일 유지
.entryTtl(Duration.ofDays(30))
.disableCachingNullValues()); // null 캐싱 방지

// 5. 식당 url 캐시 (불변 데이터, 30일 유지)
cacheConfigurations.put("cafeteriaUrl",
RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // Key Serializer
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // Value Serializer
.entryTtl(Duration.ofDays(30))
.disableCachingNullValues()); // null 캐싱 방지


Expand All @@ -60,4 +76,5 @@ public CacheManager contentCacheManager(RedisConnectionFactory cf) {
.withInitialCacheConfigurations(cacheConfigurations) // 캐시별 설정 적용
.build();
}

}
28 changes: 0 additions & 28 deletions src/main/java/com/example/Jinus/config/RestTemplateConfig.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

import com.example.Jinus.dto.request.RequestDto;
import com.example.Jinus.service.v2.cafeteria.DietServiceV2;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -16,9 +14,6 @@
public class DietControllerV2 {
private final DietServiceV2 dietServiceV2;

@Autowired
private ObjectMapper objectMapper;

@PostMapping("/v2/dish")
public String handleRequest(@RequestBody RequestDto requestDto) {
return dietServiceV2.requestHandler(requestDto);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,65 @@
package com.example.Jinus.service.v2.cafeteria;

import com.example.Jinus.config.RedisConfig;
import com.example.Jinus.dto.data.CafeteriaDto;
import com.example.Jinus.dto.data.DietDto;
import com.example.Jinus.dto.data.HandleRequestDto;
import com.example.Jinus.repository.v2.cafeteria.CafeteriaRepositoryV2;
import com.example.Jinus.repository.v2.cafeteria.DietRepositoryV2;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.sql.Date;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;

@Service
@Slf4j
@RequiredArgsConstructor
public class CacheServiceV2 {
private final DietRepositoryV2 dietRepositoryV2;
private final CafeteriaRepositoryV2 cafeteriaRepositoryV2;
private final RedisConfig redisConfig;

// @Cacheable(
// value = "dietList",
// key = "#parameters?.dietDate?.toString() + '::' + #parameters?.period + '::' + #cafeteriaId",
// unless = "#result == null || #result.isEmpty()",
// cacheManager = "contentCacheManager"
// )
// 식단 데이터 가져오기
public List<DietDto> getDietList(HandleRequestDto parameters, int cafeteriaId) {
// 오늘, 내일 문자열로 날짜 설정하기
Date dietDate = parameters.getDietDate();
// 식단 데이터 반환
return dietRepositoryV2.findDietList(dietDate, parameters.getPeriod(), cafeteriaId);
String key = parameters.getDietDate() + "::" + parameters.getPeriod() + "::" + cafeteriaId;

List<DietDto> cached = (List<DietDto>) redisConfig.redisTemplate().opsForValue().get(key);
if (cached != null) return cached;

Comment on lines +26 to +33
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prefer constructor‑injected RedisTemplate over calling the RedisConfig factory on every invocation

redisConfig.redisTemplate() is executed on every call to getDietList.
Even though the factory method is likely annotated with @Bean, Spring still has to look the bean up each time, and the indirection hides the real dependency of this service (the template, not the config).

Injecting the template directly makes dependencies explicit, avoids the per‑call lookup, and removes the need to expose the redisTemplate() factory method altogether.

-import lombok.extern.slf4j.Slf4j;
-import org.springframework.data.redis.core.RedisTemplate;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
 ...
-    private final RedisConfig redisConfig;
+    private final RedisTemplate<String, Object> redisTemplate;

Update the two call‑sites (get / set) to use redisTemplate instead of redisConfig.redisTemplate().
This keeps the service focused and eases unit testing (you can now pass a mocked template).

Committable suggestion skipped: line range outside the PR's diff.

List<DietDto> result = dietRepositoryV2.findDietList(parameters.getDietDate(), parameters.getPeriod(), cafeteriaId);

if (result != null && !result.isEmpty()) {
long ttlSeconds = calculateTtlUntilNextMidnight(parameters.getDietDate());
redisConfig.redisTemplate().opsForValue().set(key, result, Duration.ofSeconds(ttlSeconds));
}
Comment on lines +31 to +39
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Skip the SET operation when TTL ≤ 0 to avoid creating instantly‑expired keys

If calculateTtlUntilNextMidnight returns 0, the current code still executes

redisTemplate.opsForValue().set(key, result, Duration.ofSeconds(0));

With Redis this translates to a SETEX key 0, which immediately expires the key and produces unnecessary network traffic.
Guard the call with a positive‑TTL check instead:

long ttlSeconds = calculateTtlUntilNextMidnight(parameters.getDietDate());
-if (ttlSeconds > 0) {
-    redisTemplate.opsForValue().set(key, result, Duration.ofSeconds(ttlSeconds));
-}
+if (ttlSeconds > 0) {
+    redisTemplate.opsForValue().set(key, result, Duration.ofSeconds(ttlSeconds));
+} else {
+    log.debug("Diet cache not written because TTL is non‑positive for key={}", key);
+}


return result;
}

// 식단 데이터 TTL 동적 생성 -> 조회 시간과 하루 뒤의 자정시간까지의 시간차이가 TTL 시간
// 즉, 날짜가 바뀌면 만료되어야 함
private long calculateTtlUntilNextMidnight(Date dietDate) {
LocalDateTime expireAt = dietDate.toLocalDate().plusDays(1).atStartOfDay(); // dietDate + 1일 자정
LocalDateTime now = LocalDateTime.now();

Duration duration = Duration.between(now, expireAt);
long seconds = duration.getSeconds();

// 만약 이미 만료되었거나, 현재 시간이 더 크면 0 리턴 (저장 안 함)
return seconds > 0 ? seconds : 0;
}


// Redis에서 조회 (없으면 DB에서 가져옴)
// @Cacheable(value = "cafeteriaList", key = "#campusId", cacheManager = "contentCacheManager")
@Cacheable(
value = "cafeteriaList",
key = "#p0",
cacheManager = "contentCacheManager")
public List<CafeteriaDto> getCafeteriaList(int campusId) {
return cafeteriaRepositoryV2.findCafeteriaListByCampusId(campusId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@

import com.example.Jinus.repository.v2.cafeteria.CafeteriaRepositoryV2;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class CafeteriaQueryServiceV2 {
private final CafeteriaRepositoryV2 cafeteriaRepositoryV2;

// @Cacheable(value = "cafeteriaId", key = "#campusId + '::' + #cafeteriaName",
// unless = "#result == -1",
// cacheManager = "contentCacheManager")
@Cacheable(
value = "cafeteriaId",
key = "#p0 + '::' + #p1",
unless = "#result == -1",
cacheManager = "contentCacheManager")
public int getCafeteriaId(String cafeteriaName, int campusId) {
return cafeteriaRepositoryV2.findCafeteriaId(cafeteriaName, campusId).orElse(-1);
}

@Cacheable(
value = "cafeteriaUrl",
key = "#p0",
cacheManager = "contentCacheManager")
public String getImgUrl(int cafeteriaId) {
return cafeteriaRepositoryV2.findImgUrlByCafeteriaId(cafeteriaId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
import com.example.Jinus.entity.cafeteria.CampusEntity;
import com.example.Jinus.repository.v2.cafeteria.CampusRepositoryV2;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class CampusServiceV2 {
private final CampusRepositoryV2 campusRepositoryV2;

@Cacheable(
value = "campusId",
key = "#p0",
cacheManager = "contentCacheManager")
// 사용자의 campusId 받아 캠퍼스 이름 찾기
public String getUserCampusName(int campusId) {
return campusRepositoryV2.findCampusNameByCampusId(campusId);
Expand All @@ -22,7 +29,10 @@ public List<CampusEntity> getCampusList() {
return campusRepositoryV2.findCampusList();
}

// @Cacheable(value = "campusName", key = "#campusName", cacheManager = "contentCacheManager")
@Cacheable(
value = "campusName",
key = "#p0",
cacheManager = "contentCacheManager")
// 캠퍼스 이름으로 id 찾기
public int getCampusId(String campusName) {
return campusRepositoryV2.findCampusIdByName(campusName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.example.Jinus.dto.request.RequestDto;
import com.example.Jinus.service.v2.userInfo.UserServiceV2;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.sql.Date;
Expand All @@ -14,6 +15,7 @@

@Service
@RequiredArgsConstructor
@Slf4j
public class DietParameterServiceV2 {

private final UserServiceV2 userServiceV2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.example.Jinus.dto.data.HandleRequestDto;
import com.example.Jinus.repository.v2.cafeteria.DietRepositoryV2;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
Expand All @@ -15,36 +16,28 @@

@Service
@RequiredArgsConstructor
@Slf4j
public class DietQueryServiceV2 {

private final CacheServiceV2 cacheServiceV2;
private final DietRepositoryV2 dietRepositoryV2;

// 메뉴 존재 여부에 따른 반환값 처리 로직
public String getDietResponse(HandleRequestDto parameters, int cafeteriaId) {
// 메뉴 가져오기
List<DietDto> dietDtos = cacheServiceV2.getDietList(parameters, cafeteriaId);
// 메뉴 존재하는 경우
if (checkThereIsDiet(parameters, cafeteriaId) != -1) {
if (!dietDtos.isEmpty()) {
// 메뉴 찾기
MultiValueMap<String, String> dietList = getDiets(parameters, cafeteriaId);
MultiValueMap<String, String> dietList = getDiets(dietDtos);
return processDietList(dietList).toString();
}
return "\n메뉴가 존재하지 않습니다."; // 메뉴가 없는 경우
}


// 식당 메뉴 존재여부 확인
private int checkThereIsDiet(HandleRequestDto parameters, int cafeteriaId) {
// 오늘, 내일 문자열로 날짜 설정하기
Date dietDate = parameters.getDietDate();
List<DietDto> dietDtos =
dietRepositoryV2.findDietList(dietDate, parameters.getPeriod(), cafeteriaId);
return (!dietDtos.isEmpty()) ? 1 : -1;
}


// 카테고리별 메뉴 리스트 생성하기
private MultiValueMap<String, String> getDiets(HandleRequestDto parameters, int cafeteriaId) {
List<DietDto> dietDtos = cacheServiceV2.getDietList(parameters, cafeteriaId);
private MultiValueMap<String, String> getDiets(List<DietDto> dietDtos) {
MultiValueMap<String, String> dietList = new LinkedMultiValueMap<>(); // 중복 키 허용(값을 리스트로 반환)

for (DietDto o : dietDtos) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,18 @@

import com.example.Jinus.dto.data.HandleRequestDto;
import com.example.Jinus.dto.request.RequestDto;
import com.example.Jinus.repository.v2.cafeteria.DietRepositoryV2;
import com.example.Jinus.utility.DateUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;

import java.sql.Date;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Set;
import java.util.TreeSet;

@Service
@Slf4j
@RequiredArgsConstructor
public class DietServiceV2 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.example.Jinus.repository.v2.userInfo.UserRepositoryV2;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

Expand All @@ -11,6 +12,7 @@

@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceV2 {

private final UserRepositoryV2 userRepositoryV2;
Expand Down
Loading