-
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 all commits
0ec7829
e491e58
e8db4a3
2c58b3e
5a04bcc
05bd9aa
0833164
46b6964
bafe21f
c75d618
aabab7b
ff17e5a
05505b7
3816476
8b857b1
3e903a4
a63d14c
ae444db
58e6e4e
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,80 @@ | ||
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); | ||
validateCacheNotNull(key, cache); | ||
String value = cache.get(key, String.class); | ||
log.debug("CACHE NAME: {}, KEY: {}, STATE: HIT", cacheName, key); | ||
return convertObject(cacheName, key, clazz, value); | ||
} | ||
|
||
private void validateCacheNotNull(String key, Cache cache) { | ||
if (cache == null || cache.get(key, String.class) == null) { | ||
throw new MomoException(CacheErrorCode.CACHE_NOT_FOUND); | ||
} | ||
} | ||
|
||
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); | ||
} | ||
} |
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 이상을 유지하고 있고 주기적으로 업데이트를 거치며 잠재적인 취약점을 개선하고 있음을 확인할 수 있었기에 해당 의존성을 사용하였습니다.