Skip to content
Open
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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/inu/codin/codin/common/config/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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")
Expand All @@ -20,7 +28,8 @@ public class CalendarDayResponse {
@Schema(description = "해당 날짜의 이벤트 목록")
private final List<EventDto> items;

public CalendarDayResponse(LocalDate date, int totalCont, List<EventDto> items) {
@JsonCreator
public CalendarDayResponse(@JsonProperty("date") LocalDate date, @JsonProperty("totalCont") int totalCont, @JsonProperty("items") List<EventDto> items) {
this.date = date;
this.totalCont = totalCont;
this.items = items;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,7 +21,8 @@ public class CalendarMonthResponse {
private final List<CalendarDayResponse> days;

@Builder
public CalendarMonthResponse(int year, int month, List<CalendarDayResponse> days) {
@JsonCreator
public CalendarMonthResponse(@JsonProperty("year") int year, @JsonProperty("month") int month, @JsonProperty("days") List<CalendarDayResponse> days) {
this.year = year;
this.month = month;
this.days = days;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Comment on lines +28 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

@Cacheable에서 사용하는 캐시 이름은 컴파일 타임 상수여야 합니다

현재 private final String CACHE_NAME는 주석 위치에서 컴파일 타임 상수가 아니라서 어노테이션 인자로 사용할 수 없어 컴파일 오류가 납니다. static final로 승격하거나 문자열 리터럴을 직접 사용하세요.

-    private final String CACHE_NAME =  "calendar";
+    private static final String CACHE_NAME = "calendar";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private final String CACHE_NAME = "calendar";
private final CacheManager cacheManager;
private static final String CACHE_NAME = "calendar";
private final CacheManager cacheManager;
🤖 Prompt for AI Agents
In src/main/java/inu/codin/codin/domain/calendar/service/CalendarService.java
around lines 28 to 30, the CACHE_NAME field is declared as a non-compile-time
constant (private final String CACHE_NAME) but is used in @Cacheable which
requires a compile-time constant; change the declaration to a compile-time
constant by promoting it to private static final String CACHE_NAME = "calendar"
(or replace usages in annotations with the string literal "calendar") so the
annotation parameter becomes a constant that compiles.

/**
* 캘린더 조회
* + 조회 캐시 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) {
Comment on lines +38 to 41
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

키 SpEL에서 YearMonth.of 사용 시 유효성 검사 이전에 예외 발생 가능 + value 속성 사용법

  • 현재 키: T(java.time.YearMonth).of(#year, #month)는 메서드 본문 유효성 검사(1~12) 이전에 SpEL 평가 중 DateTimeException을 유발할 수 있습니다.
  • 또, 어노테이션 속성은 상수여야 하므로 위의 CACHE_NAME 수정과 함께 cacheNames 속성 사용을 권장합니다.

안전한 키 문자열 포맷을 사용하세요.

-    @Cacheable(value = CACHE_NAME, key = "'month:' + T(java.time.YearMonth).of(#year, #month)")
+    @Cacheable(cacheNames = CACHE_NAME, key = "T(java.lang.String).format('month:%04d-%02d', #year, #month)")
-    public CalendarMonthResponse getMonth(int year, int month) {
-        log.info("Getting month for year {} and month {}", year, month);
+    public CalendarMonthResponse getMonth(int year, int month) {
+        log.info("Getting month for year {} and month {}", year, month);

설명:

  • 키는 month:YYYY-MM 형태로 패딩되어 evictMonthBetween"month:" + ym와 동일 포맷을 유지합니다.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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) {
@Cacheable(cacheNames = CACHE_NAME, key = "T(java.lang.String).format('month:%04d-%02d', #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);
}
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
}
}