-
Notifications
You must be signed in to change notification settings - Fork 8
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
base: develop
Are you sure you want to change the base?
Changes from 16 commits
ab297e1
dbec7a2
4b8bee4
061d71a
66abf4a
ad6d842
bac5d47
318ae4a
c2c419d
db5831b
f92f6c1
ffa87cf
b05849f
de8f1d3
8d54c75
5b39a04
c01f4fe
3f44fde
6a559de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(); | ||
} | ||
} | ||
} |
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(); | ||
} | ||
} |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 클래스는 왜 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 외부 의존성(캐시)를 사용하는 메서드를 다루는 객체이다 보니 도메인에 두기에 어색하다 생각했습니다. |
||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 메서드로 분리하는게 좋아 보여요! |
||
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 |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package kr.momo.config; | ||
|
||
import groovy.util.logging.Slf4j; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
codemonstur
의 fork를 사용한 이유가 있나요?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maven repository에서
embedded redis
로 유명한 라이브러리는 2개가 있습니다.com.github.kstyrc
it.ozimov
두 의존성 모두 정식 출시되지 않는 major version 1이 출시되지 않았으며, 업데이트 기간이 5년 이상 지난 라이브러리로 식별하였습니다.
그로 인해 라이브러리 사용으로 인해 발생할 수 있는 CVE에 등록된 취약점들이 해결되지 않는 것을 확인할 수 있었습니다.
현재 사용하고 있는 라이브러리('com.github.codemonstur:embedded-redis')는 위의 1번 com.github.kstyrc의 fork 기반으로 버전 1 이상을 유지하고 있고 주기적으로 업데이트를 거치며 잠재적인 취약점을 개선하고 있음을 확인할 수 있었기에 해당 의존성을 사용하였습니다.