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

[BE] 약속 조회와 약속 추천 서버 캐시 추가 :) #437

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ dependencies {
implementation 'org.springframework.security:spring-security-crypto'
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'com.github.codemonstur:embedded-redis:1.4.3'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

Expand Down
32 changes: 32 additions & 0 deletions backend/src/main/java/kr/momo/config/EmbeddedRedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package kr.momo.config;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.io.IOException;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import redis.embedded.RedisServer;

@Configuration
@Profile("local")
public class EmbeddedRedisConfig {

private final RedisServer redisServer;

public EmbeddedRedisConfig(RedisProperties redisProperties) throws IOException {
this.redisServer = new RedisServer(redisProperties.getPort());
}

@PostConstruct
public void start() throws IOException {
redisServer.start();
}

@PreDestroy
public void stop() throws IOException {
if (redisServer.isActive()) {
redisServer.stop();
}
}
}
32 changes: 32 additions & 0 deletions backend/src/main/java/kr/momo/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package kr.momo.config;

import java.time.Duration;
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.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.RedisSerializer;

@EnableCaching
@Configuration
public class RedisConfig {

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration cacheConfiguration = getCacheConfiguration();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(cacheConfiguration)
.build();
}

private RedisCacheConfiguration getCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(SerializationPair.fromSerializer(RedisSerializer.string()))
.serializeValuesWith(SerializationPair.fromSerializer(RedisSerializer.json()))
.entryTtl(Duration.ofMinutes(10))
.enableTimeToIdle();
}
}
14 changes: 14 additions & 0 deletions backend/src/main/java/kr/momo/config/constant/CacheType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package kr.momo.config.constant;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum CacheType {

SCHEDULES_STORE("schedules-store"),
RECOMMEND_STORE("recommend-store");

private final String name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package kr.momo.exception.code;

import org.springframework.http.HttpStatus;

public enum CacheErrorCode implements ErrorCodeType {

CACHE_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "데이터 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."),
CACHE_JSON_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "데이터 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."),
DATA_DESERIALIZATION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "데이터 변환 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");

private final HttpStatus httpStatus;
private final String message;

CacheErrorCode(HttpStatus httpStatus, String message) {
this.httpStatus = httpStatus;
this.message = message;
}

@Override
public HttpStatus httpStatus() {
return httpStatus;
}

@Override
public String message() {
return message;
}

@Override
public String errorCode() {
return name();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package kr.momo.service.schedule;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import kr.momo.config.constant.CacheType;
import kr.momo.exception.MomoException;
import kr.momo.exception.code.CacheErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class ScheduleCache {

public static final String INVALID_STATUS = "invalid";

private final ObjectMapper objectMapper;
private final CacheManager cacheManager;

public boolean isHit(CacheType cacheType, String key) {
Cache cache = cacheManager.getCache(cacheType.getName());
if (cache == null) {
return false;
}
String json = cache.get(key, String.class);
return json != null && !INVALID_STATUS.equals(json);
}

public <T> T get(CacheType cacheType, String key, Class<T> clazz) {
String cacheName = cacheType.getName();
Cache cache = cacheManager.getCache(cacheName);
if (cache == null || cache.get(key, String.class) == null) {
throw new MomoException(CacheErrorCode.CACHE_NOT_FOUND);
}
String value = cache.get(key, String.class);
log.debug("CACHE NAME: {}, KEY: {}, STATE: HIT", cacheName, key);
return convertObject(cacheName, key, clazz, value);
}

private <T> T convertObject(String cacheName, String key, Class<T> clazz, String value) {
try {
return objectMapper.readValue(value, clazz);
} catch (JsonProcessingException e) {
log.error("캐시 값을 JSON으로 변환하는데 실패했습니다. CACHE NAME: {}, KEY: {}", cacheName, key);
throw new MomoException(CacheErrorCode.CACHE_JSON_PROCESSING_ERROR);
}
}

public <T> void put(CacheType cacheType, String key, T value) {
String cacheName = cacheType.getName();
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
log.error("캐싱에 해당하는 이름이 존재하지 않습니다. 캐싱 이름: {}", cacheName);
return;
}
log.debug("CACHE NAME: {}, KEY: {}, STATE: MISS", cacheName, key);
cache.put(key, convertToJson(cacheName, key, value));
}

private <T> String convertToJson(String cacheName, String key, T value) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
log.error("캐시 값을 객체로 변환하는데 실패했습니다. CACHE NAME: {}, KEY: {}", cacheName, key);
throw new MomoException(CacheErrorCode.DATA_DESERIALIZATION_ERROR);
}
}

public void putInvalid(CacheType cacheType, String key) {
put(cacheType, key, INVALID_STATUS);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package kr.momo.service.schedule;

import java.time.LocalTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import kr.momo.config.constant.CacheType;
import kr.momo.domain.attendee.Attendee;
import kr.momo.domain.attendee.AttendeeGroup;
import kr.momo.domain.attendee.AttendeeName;
Expand All @@ -16,6 +18,7 @@
import kr.momo.domain.schedule.ScheduleBatchRepository;
import kr.momo.domain.schedule.ScheduleRepository;
import kr.momo.domain.schedule.recommend.CandidateSchedule;
import kr.momo.domain.schedule.recommend.RecommendedScheduleSortStandard;
import kr.momo.domain.timeslot.Timeslot;
import kr.momo.exception.MomoException;
import kr.momo.exception.code.AttendeeErrorCode;
Expand Down Expand Up @@ -43,6 +46,7 @@ public class ScheduleService {
private final AvailableDateRepository availableDateRepository;
private final ScheduleBatchRepository scheduleBatchRepository;
private final ScheduleRecommenderFactory scheduleRecommenderFactory;
private final ScheduleCache scheduleCache;

@Transactional
public void create(String uuid, long attendeeId, ScheduleCreateRequest request) {
Expand All @@ -56,6 +60,10 @@ public void create(String uuid, long attendeeId, ScheduleCreateRequest request)
scheduleRepository.deleteByAttendee(attendee);
List<Schedule> schedules = createSchedules(request, meeting, attendee);
scheduleBatchRepository.batchInsert(schedules);
scheduleCache.putInvalid(CacheType.SCHEDULES_STORE, uuid);
Arrays.stream(RecommendedScheduleSortStandard.values())
.map(RecommendedScheduleSortStandard::getType)
.forEach(type -> scheduleCache.putInvalid(CacheType.RECOMMEND_STORE, type + uuid));
}

private void validateMeetingUnLocked(Meeting meeting) {
Expand Down Expand Up @@ -89,12 +97,19 @@ private Schedule createSchedule(Meeting meeting, Attendee attendee, AvailableDat

@Transactional(readOnly = true)
public SchedulesResponse findAllSchedules(String uuid) {
if (scheduleCache.isHit(CacheType.SCHEDULES_STORE, uuid)) {
return scheduleCache.get(CacheType.SCHEDULES_STORE, uuid, SchedulesResponse.class);
}

Meeting meeting = meetingRepository.findByUuid(uuid)
.orElseThrow(() -> new MomoException(MeetingErrorCode.NOT_FOUND_MEETING));
List<Attendee> attendees = attendeeRepository.findAllByMeeting(meeting);
List<Schedule> schedules = scheduleRepository.findAllByAttendeeIn(attendees);
SchedulesResponse schedulesResponse = SchedulesResponse.from(schedules);

return SchedulesResponse.from(schedules);
scheduleCache.put(CacheType.SCHEDULES_STORE, uuid, schedulesResponse);

return schedulesResponse;
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -125,6 +140,10 @@ public AttendeeScheduleResponse findMySchedule(String uuid, long attendeeId) {
public RecommendedSchedulesResponse recommendSchedules(
String uuid, String recommendType, List<String> names, int minimumTime
) {
String key = recommendType + uuid;
if (scheduleCache.isHit(CacheType.RECOMMEND_STORE, key)) {
return scheduleCache.get(CacheType.RECOMMEND_STORE, key, RecommendedSchedulesResponse.class);
}
Meeting meeting = meetingRepository.findByUuid(uuid)
.orElseThrow(() -> new MomoException(MeetingErrorCode.NOT_FOUND_MEETING));
AttendeeGroup attendeeGroup = new AttendeeGroup(attendeeRepository.findAllByMeeting(meeting));
Expand All @@ -140,6 +159,12 @@ public RecommendedSchedulesResponse recommendSchedules(
List<RecommendedScheduleResponse> scheduleResponses = RecommendedScheduleResponse.fromCandidateSchedules(
recommendedResult
);
return RecommendedSchedulesResponse.of(meeting.getType(), scheduleResponses);
RecommendedSchedulesResponse recommendedSchedulesResponse = RecommendedSchedulesResponse.of(
meeting.getType(), scheduleResponses
);

scheduleCache.put(CacheType.RECOMMEND_STORE, key, recommendedSchedulesResponse);

return recommendedSchedulesResponse;
}
}
1 change: 1 addition & 0 deletions backend/src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ spring:
- ${SECURITY_PATH:classpath:security}/cors.yml
- ${SECURITY_PATH:classpath:security}/logback.yml
- ${SECURITY_PATH:classpath:security}/actuator.yml
- ${SECURITY_PATH:classpath:security}/cache-dev.yml

jpa:
hibernate:
Expand Down
8 changes: 7 additions & 1 deletion backend/src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ spring:
activate.on-profile: local
import:
- classpath:datasource.yml

jpa:
hibernate:
ddl-auto: create
Expand All @@ -19,6 +18,13 @@ spring:
path: /h2-console
settings:
web-allow-others: true
cache:
type: redis
data:
redis:
host: localhost
port: ${REDIS_PORT:6379}
timeout: 2000

security:
jwt:
Expand Down
1 change: 1 addition & 0 deletions backend/src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ spring:
- ${SECURITY_PATH:classpath:security}/cors.yml
- ${SECURITY_PATH:classpath:security}/logback.yml
- ${SECURITY_PATH:classpath:security}/actuator.yml
- ${SECURITY_PATH:classpath:security}/cache-prod.yml

jpa:
hibernate:
Expand Down
2 changes: 1 addition & 1 deletion backend/src/main/resources/security
68 changes: 68 additions & 0 deletions backend/src/test/java/kr/momo/config/PortKiller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package kr.momo.config;

import groovy.util.logging.Slf4j;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.springframework.boot.test.context.TestComponent;

@Slf4j
@TestComponent
public class PortKiller {

private static final String OS_NAME = System.getProperty("os.name").toLowerCase();
private static final String WIN_PORT_FIND_COMMAND = "netstat -ano | findstr :%d";
private static final String NOT_WIN_PORT_FIND_COMMAND = "lsof -i :%d";
private static final String WIN_PROCESS_KILL_COMMAND = "taskkill /PID %s /F";
private static final String NOT_WIN_PROCESS_KILL_COMMAND = "kill -9 %s";
private static final boolean IS_OS_WINDOW = OS_NAME.contains("win");

public void killProcessUsingPort(int port) {
try {
String pid = getProcessIdUsingPort(port);
if (pid != null) {
killProcess(pid);
}
} catch (Exception e) {
System.err.println("포트 종료 중 오류 발생: " + e.getMessage());
}
}

private String getProcessIdUsingPort(int port) throws Exception {
String command = getFindPortCommand(port);
Process process = Runtime.getRuntime().exec(command);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
if ((line = reader.readLine()) != null) {
return parseUsingPort(line);
}
}
return null;
}

private String parseUsingPort(String line) {
if (IS_OS_WINDOW) {
return line.trim().split("\\s+")[4];
}
return line.trim().split("\\s+")[1];
}

private String getFindPortCommand(int port) {
if (IS_OS_WINDOW) {
return WIN_PORT_FIND_COMMAND.formatted(port);
}
return NOT_WIN_PORT_FIND_COMMAND.formatted(port);
}

private void killProcess(String pid) throws Exception {
String command = getKillCommand(pid);
Process process = Runtime.getRuntime().exec(command);
process.waitFor();
}

private String getKillCommand(String pid) {
if (IS_OS_WINDOW) {
return WIN_PROCESS_KILL_COMMAND.formatted(pid);
}
return NOT_WIN_PROCESS_KILL_COMMAND.formatted(pid);
}
}
Loading