diff --git a/build.gradle b/build.gradle index 5ddd651f..09cbaac6 100644 --- a/build.gradle +++ b/build.gradle @@ -72,6 +72,8 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-core:2.16.1' //oauth2 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + //Cache + implementation 'org.springframework.boot:spring-boot-starter-cache' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/inu/codin/codin/common/config/CacheConfig.java b/src/main/java/inu/codin/codin/common/config/CacheConfig.java new file mode 100644 index 00000000..a1afe8b0 --- /dev/null +++ b/src/main/java/inu/codin/codin/common/config/CacheConfig.java @@ -0,0 +1,33 @@ +package inu.codin.codin.common.config; + +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.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; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@EnableCaching +@Configuration +public class CacheConfig { + + @Bean + public CacheManager cacheManager(RedisConnectionFactory cf) { + RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) + .entryTtl(Duration.ofMinutes(30)); + + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(cf) + .cacheDefaults(redisCacheConfiguration) + .build(); + } + +} diff --git a/src/main/java/inu/codin/codin/domain/calendar/dto/CalendarDayResponse.java b/src/main/java/inu/codin/codin/domain/calendar/dto/CalendarDayResponse.java index 5e46b15d..e16c4122 100644 --- a/src/main/java/inu/codin/codin/domain/calendar/dto/CalendarDayResponse.java +++ b/src/main/java/inu/codin/codin/domain/calendar/dto/CalendarDayResponse.java @@ -1,6 +1,12 @@ package inu.codin.codin.domain.calendar.dto; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; @@ -12,6 +18,8 @@ public class CalendarDayResponse { @Schema(description = "날짜", example = "2025-08-23") @JsonFormat(pattern = "yyyy-MM-dd") + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) private final LocalDate date; @Schema(description = "해당 날짜의 총 이벤트 수", example = "2") @@ -20,7 +28,8 @@ public class CalendarDayResponse { @Schema(description = "해당 날짜의 이벤트 목록") private final List items; - public CalendarDayResponse(LocalDate date, int totalCont, List items) { + @JsonCreator + public CalendarDayResponse(@JsonProperty("date") LocalDate date, @JsonProperty("totalCont") int totalCont, @JsonProperty("items") List items) { this.date = date; this.totalCont = totalCont; this.items = items; diff --git a/src/main/java/inu/codin/codin/domain/calendar/dto/CalendarMonthResponse.java b/src/main/java/inu/codin/codin/domain/calendar/dto/CalendarMonthResponse.java index b812cf11..ac8b7f40 100644 --- a/src/main/java/inu/codin/codin/domain/calendar/dto/CalendarMonthResponse.java +++ b/src/main/java/inu/codin/codin/domain/calendar/dto/CalendarMonthResponse.java @@ -1,5 +1,7 @@ package inu.codin.codin.domain.calendar.dto; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; @@ -19,7 +21,8 @@ public class CalendarMonthResponse { private final List days; @Builder - public CalendarMonthResponse(int year, int month, List days) { + @JsonCreator + public CalendarMonthResponse(@JsonProperty("year") int year, @JsonProperty("month") int month, @JsonProperty("days") List days) { this.year = year; this.month = month; this.days = days; diff --git a/src/main/java/inu/codin/codin/domain/calendar/dto/EventDto.java b/src/main/java/inu/codin/codin/domain/calendar/dto/EventDto.java index f6652180..4fa2577c 100644 --- a/src/main/java/inu/codin/codin/domain/calendar/dto/EventDto.java +++ b/src/main/java/inu/codin/codin/domain/calendar/dto/EventDto.java @@ -1,5 +1,7 @@ package inu.codin.codin.domain.calendar.dto; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import inu.codin.codin.common.dto.Department; import inu.codin.codin.common.util.ObjectIdUtil; import inu.codin.codin.domain.calendar.entity.CalendarEntity; @@ -20,7 +22,8 @@ public class EventDto { private final Department department; @Builder - public EventDto(String eventId, String content, Department department) { + @JsonCreator + public EventDto(@JsonProperty("eventId") String eventId, @JsonProperty("content") String content, @JsonProperty("department") Department department) { this.eventId = eventId; this.content = content; this.department = department; diff --git a/src/main/java/inu/codin/codin/domain/calendar/service/CalendarService.java b/src/main/java/inu/codin/codin/domain/calendar/service/CalendarService.java index 73389f3d..9681b6d2 100644 --- a/src/main/java/inu/codin/codin/domain/calendar/service/CalendarService.java +++ b/src/main/java/inu/codin/codin/domain/calendar/service/CalendarService.java @@ -7,19 +7,37 @@ import inu.codin.codin.domain.calendar.exception.CalendarException; import inu.codin.codin.domain.calendar.repository.CalendarRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.bson.types.ObjectId; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.time.LocalDate; +import java.time.YearMonth; import java.util.*; +@Slf4j @Service @RequiredArgsConstructor public class CalendarService { private final CalendarRepository calendarRepository; + private final String CACHE_NAME = "calendar"; + private final CacheManager cacheManager; + + /** + * 캘린더 조회 + * + 조회 캐시 Write-Through 전략 사용 + * @param year 년도 ex) 2025 + * @param month 월 ex) 8 + * @return CalendarMonthResponse + */ + @Cacheable(value = CACHE_NAME, key = "'month:' + T(java.time.YearMonth).of(#year, #month)") public CalendarMonthResponse getMonth(int year, int month) { + log.info("Getting month for year {} and month {}", year, month); if (month < 1 || month > 12) { throw new CalendarException(CalendarErrorCode.DATE_FORMAT_ERROR); } @@ -61,6 +79,12 @@ public CalendarMonthResponse getMonth(int year, int month) { .build(); } + /** + * 켈린더 이벤트 생성 + * + 기존 엔티티의 시간을 기준으로 캐시를 무효화 + * @param request CalendarCreateRequest + * @return CalendarCreateResponse + */ public CalendarCreateResponse create(CalendarCreateRequest request) { if (request.getStartDate() == null || request.getEndDate() == null) { throw new CalendarException(CalendarErrorCode.DATE_CANNOT_NULL); @@ -77,14 +101,46 @@ public CalendarCreateResponse create(CalendarCreateRequest request) { .build(); CalendarEntity savedEntity = calendarRepository.save(entity); + evictMonthBetween(savedEntity.getStartDate(), savedEntity.getEndDate()); + return CalendarCreateResponse.of(savedEntity); } + /** + * 켈린더 이벤트 삭제(SoftDelete) + * + 기존 엔티티의 시간을 기준으로 캐시를 무효화 + * @param id + */ public void delete(String id) { ObjectId objectId = ObjectIdUtil.toObjectId(id); CalendarEntity calendar = calendarRepository.findByIdAndNotDeleted(objectId) .orElseThrow(() -> new CalendarException(CalendarErrorCode.CALENDAR_EVENT_NOT_FOUND)); + calendar.delete(); calendarRepository.save(calendar); + + LocalDate start = calendar.getStartDate(); + LocalDate end = calendar.getEndDate(); + evictMonthBetween(start, end); + } + + /** + * 캐시 무효화 + * @param startDate + * @param endDate + */ + private void evictMonthBetween(LocalDate startDate, LocalDate endDate) { + if (startDate == null || endDate == null) return; + Cache cache = cacheManager.getCache(CACHE_NAME); + if (cache == null) return; + + YearMonth from = YearMonth.from(startDate); + YearMonth to = YearMonth.from(endDate); + + for (YearMonth ym = from; !ym.isAfter(to); ym = ym.plusMonths(1)) { + String key = "month:" + ym; + log.info("Evicting month {} from {} to {}", ym, from, to); + cache.evictIfPresent(key); + } } }