Skip to content

Commit

Permalink
[UNI-255] refactor: 캐시를 활용한 응답 시간 개선 & 직렬화 시간 개선 (#197)
Browse files Browse the repository at this point in the history
* [UNI-255] feat: redis 환경 세팅

* [UNI-255] feat: heap 사이즈 500으로 조정

* [UNI-255] refactor: 경량 객체 구현

* [UNI-255] feat: 경량 객체에 사용되는 계산기 구현

* [UNI-255] feat: redis 서비스 구현

* [UNI-255] feat: 로컬 캐시 버전 구현

* [UNI-255] feat: 캐시 무효화 구현

* [UNI-255] feat: 롤백시 캐시 무효화 구현

* [UNI-255] refactor: fastJson 직렬화 구현

* [UNI-255] test

* [UNI-255] fix: json 지원

* [UNI-255] refactor: 캐시 로직 적용

* [UNI-255] fix: redis 컨테이너 추가

* [UNI-255] fix: redis 컨테이너 추가 & 필요없는 설정 제거

* [UNI-255] test cd 삭제

* [UNI-255] fix: redis 컨테이너 설정 수정

* [UNI-255] fix: redis 컨테이너 설정 수정 해결

* [UNI-255] fix: 필요없는 설정 제거
  • Loading branch information
mikekks authored Feb 20, 2025
1 parent 2c36757 commit 884e2a7
Show file tree
Hide file tree
Showing 16 changed files with 449 additions and 17 deletions.
2 changes: 1 addition & 1 deletion uniro_backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ COPY ${JAR_FILE} uniro-server.jar
ENV SPRING_PROFILE=${SPRING_PROFILE}

# JVM 메모리 설정 추가
ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul","-Dspring.profiles.active=${SPRING_PROFILE}","-XX:+HeapDumpOnOutOfMemoryError","-XX:HeapDumpPath=/tmp/heapdump.hprof","-jar", "uniro-server.jar"]
ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul","-Dspring.profiles.active=${SPRING_PROFILE}","-XX:+HeapDumpOnOutOfMemoryError","-XX:HeapDumpPath=/tmp/heapdump.hprof", "-Xmx500m", "-jar", "uniro-server.jar"]

6 changes: 6 additions & 0 deletions uniro_backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ dependencies {
implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
implementation 'com.auth0:java-jwt:4.4.0'

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate6'
implementation 'com.alibaba.fastjson2:fastjson2-extension-spring6:2.0.55'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.softeer5.uniro_backend.common.exception.custom.AdminException;
import com.softeer5.uniro_backend.common.exception.custom.RouteException;
import com.softeer5.uniro_backend.common.exception.custom.UnivException;
import com.softeer5.uniro_backend.common.redis.RedisService;
import com.softeer5.uniro_backend.map.dto.response.AllRoutesInfo;
import com.softeer5.uniro_backend.map.dto.response.GetChangedRoutesByRevisionResDTO;
import com.softeer5.uniro_backend.map.dto.response.GetRiskRoutesResDTO;
Expand Down Expand Up @@ -45,6 +46,8 @@ public class AdminService {

private final RouteCalculator routeCalculator;

private final RedisService redisService;

public List<RevInfoDTO> getAllRevInfo(Long univId){
return revInfoRepository.findAllByUnivId(univId).stream().map(r -> RevInfoDTO.of(r.getRev(),
r.getRevTimeStamp(),
Expand Down Expand Up @@ -105,6 +108,7 @@ public void rollbackRev(Long univId, Long versionId){
}
}

redisService.deleteData(univId.toString());
}

public GetAllRoutesByRevisionResDTO getAllRoutesByRevision(Long univId, Long versionId){
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
package com.softeer5.uniro_backend.common.config;

import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.support.config.FastJsonConfig;
import com.alibaba.fastjson2.support.spring6.http.converter.FastJsonHttpMessageConverter;
import com.softeer5.uniro_backend.admin.interceptor.AdminInterceptor;
import com.softeer5.uniro_backend.admin.interceptor.JwtInterceptor;

import lombok.extern.slf4j.Slf4j;

@Configuration
@EnableWebMvc
@Slf4j
public class WebMvcConfig implements WebMvcConfigurer {
private final AdminInterceptor adminInterceptor;
private final JwtInterceptor jwtInterceptor; // JWT 인터셉터 추가
Expand All @@ -18,6 +33,27 @@ public WebMvcConfig(AdminInterceptor adminInterceptor, JwtInterceptor jwtInterce
this.jwtInterceptor = jwtInterceptor;
}

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
//custom configuration...
FastJsonConfig config = new FastJsonConfig();
config.setDateFormat("yyyy-MM-dd HH:mm:ss");
config.setReaderFeatures(JSONReader.Feature.FieldBased, JSONReader.Feature.SupportArrayToBean);
config.setWriterFeatures(JSONWriter.Feature.WriteMapNullValue, JSONWriter.Feature.PrettyFormat);
converter.setFastJsonConfig(config);
converter.setDefaultCharset(StandardCharsets.UTF_8);

converter.setSupportedMediaTypes(List.of(
MediaType.APPLICATION_JSON, // application/json 지원
new MediaType("application", "json", StandardCharsets.UTF_8),
new MediaType("application", "openmetrics-text", StandardCharsets.UTF_8)
));

converters.add(0, converter);
converters.forEach(c -> log.info("✔ {}", c.getClass().getName()));
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
// JWT 인터셉터는 더 먼저 실행되도록 우선순위 낮춤
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.softeer5.uniro_backend.common.redis;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.alibaba.fastjson2.support.spring6.data.redis.FastJsonRedisSerializer;
import com.softeer5.uniro_backend.map.service.vo.LightRoutes;

@Configuration
public class RedisConfig {

@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

// 키는 String, 값은 FastJson으로 변환된 JSON 문자열 저장
StringRedisSerializer serializer = new StringRedisSerializer();
FastJsonRedisSerializer<LightRoutes> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(LightRoutes.class);

template.setKeySerializer(serializer);
template.setValueSerializer(fastJsonRedisSerializer);

return template;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.softeer5.uniro_backend.common.redis;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@RequiredArgsConstructor
@Slf4j
public class RedisService {

private final RedisTemplate redisTemplate;
private final Map<String, Boolean> cacheMap = new HashMap<>();

public void saveData(String key, Object value) {
long startTime = System.nanoTime();
redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(1000)); // 10분 TTL
long endTime = System.nanoTime();

long duration = TimeUnit.NANOSECONDS.toMicros(endTime - startTime);
log.info("🔴🔴🔴🔴🔴🔴🔴 Redis 직렬화 및 저장 시간: {} 마이크로초", duration);

cacheMap.put(key, true);
}

public Object getData(String key) {
long startTime = System.nanoTime();
Object data = redisTemplate.opsForValue().get(key);
long endTime = System.nanoTime();

long duration = TimeUnit.NANOSECONDS.toMicros(endTime - startTime);
log.info("🟢🟢🟢🟢🟢🟢🟢 Redis 역직렬화 및 조회 시간: {} 마이크로초", duration);
cacheMap.put(key, true);
return data;
}

public boolean hasData(String key){
return cacheMap.containsKey(key);
}

public void deleteData(String key) {
redisTemplate.delete(key);
cacheMap.remove(key);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public class MapController implements MapApi {
private final MapService mapService;
private final AdminService adminService;

@GetMapping("/{univId}/routes-local")
public ResponseEntity<GetAllRoutesResDTO> getAllRoutesAndNodesByLocalCache(@PathVariable("univId") Long univId){
GetAllRoutesResDTO allRoutes = mapService.getAllRoutesByLocalCache(univId);
return ResponseEntity.ok().body(allRoutes);
}

@Override
@GetMapping("/{univId}/routes")
public ResponseEntity<GetAllRoutesResDTO> getAllRoutesAndNodes(@PathVariable("univId") Long univId){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.softeer5.uniro_backend.common.exception.custom.NodeException;
import com.softeer5.uniro_backend.common.exception.custom.RouteCalculationException;
import com.softeer5.uniro_backend.common.exception.custom.RouteException;
import com.softeer5.uniro_backend.common.redis.RedisService;
import com.softeer5.uniro_backend.external.MapClient;
import com.softeer5.uniro_backend.map.dto.request.CreateRoutesReqDTO;
import com.softeer5.uniro_backend.map.entity.Node;
Expand All @@ -36,6 +37,8 @@
import com.softeer5.uniro_backend.map.dto.request.PostRiskReqDTO;
import com.softeer5.uniro_backend.map.entity.Route;
import com.softeer5.uniro_backend.map.repository.RouteRepository;
import com.softeer5.uniro_backend.map.service.vo.LightRoute;
import com.softeer5.uniro_backend.map.service.vo.LightRoutes;

import lombok.RequiredArgsConstructor;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
Expand All @@ -52,24 +55,66 @@ public class MapService {
private final RevInfoRepository revInfoRepository;

private final RouteCalculator routeCalculator;
private final RouteCacheCalculator routeCacheCalculator;

private final MapClient mapClient;

private final RedisService redisService;

private final Map<Long, List<LightRoute>> cache = new HashMap<>();

public GetAllRoutesResDTO getAllRoutesByLocalCache(Long univId) {

if(!cache.containsKey(univId)){
List<Route> routes = routeRepository.findAllRouteByUnivIdWithNodes(univId);
List<LightRoute> lightRoutes = routes.stream().map(LightRoute::new).toList();
cache.put(univId, lightRoutes);
}

List<LightRoute> routes = cache.get(univId);

// 맵이 존재하지 않을 경우 예외
if(routes.isEmpty()) {
throw new RouteException("Route Not Found", ROUTE_NOT_FOUND);
}

RevInfo revInfo = revInfoRepository.findFirstByUnivIdOrderByRevDesc(univId)
.orElseThrow(() -> new RouteException("Revision not found", RECENT_REVISION_NOT_FOUND));

AllRoutesInfo allRoutesInfo = routeCacheCalculator.assembleRoutes(routes);
GetAllRoutesResDTO response = GetAllRoutesResDTO.of(allRoutesInfo.getNodeInfos(), allRoutesInfo.getCoreRoutes(),
allRoutesInfo.getBuildingRoutes(), revInfo.getRev());

return response;
}

public GetAllRoutesResDTO getAllRoutes(Long univId) {
List<Route> routes = routeRepository.findAllRouteByUnivIdWithNodes(univId);

if(!redisService.hasData(univId.toString())){
List<Route> routes = routeRepository.findAllRouteByUnivIdWithNodes(univId);
List<LightRoute> lightRoutes = routes.stream().map(LightRoute::new).toList();
LightRoutes value = new LightRoutes(lightRoutes);
redisService.saveData(univId.toString(), value);
}
else{
log.info("🚀🚀🚀🚀🚀🚀🚀HIT🚀🚀🚀🚀🚀🚀🚀");
}

LightRoutes lightRoutes = (LightRoutes) redisService.getData(univId.toString());
List<LightRoute> routes = lightRoutes.getLightRoutes();

// 맵이 존재하지 않을 경우 예외
if(routes.isEmpty()) {
throw new RouteException("Route Not Found", ROUTE_NOT_FOUND);
}

RevInfo revInfo = revInfoRepository.findFirstByUnivIdOrderByRevDesc(univId)
.orElseThrow(() -> new RouteException("Revision not found", RECENT_REVISION_NOT_FOUND));
.orElseThrow(() -> new RouteException("Revision not found", RECENT_REVISION_NOT_FOUND));

AllRoutesInfo allRoutesInfo = routeCalculator.assembleRoutes(routes);
AllRoutesInfo allRoutesInfo = routeCacheCalculator.assembleRoutes(routes);

return GetAllRoutesResDTO.of(allRoutesInfo.getNodeInfos(), allRoutesInfo.getCoreRoutes(),
allRoutesInfo.getBuildingRoutes(), revInfo.getRev());
allRoutesInfo.getBuildingRoutes(), revInfo.getRev());
}

@Async
Expand Down Expand Up @@ -204,7 +249,7 @@ public void createBuildingRoute(Long univId, CreateBuildingRouteReqDTO createBui

@Transactional
@RevisionOperation(RevisionOperationType.CREATE_ROUTE)
synchronized public AllRoutesInfo createRoute(Long univId, CreateRoutesReqDTO requests){
public synchronized AllRoutesInfo createRoute(Long univId, CreateRoutesReqDTO requests){

if(requests.getCoordinates().size() >= CREATE_ROUTE_LIMIT_COUNT){
throw new RouteException("creat route limit exceeded", CREATE_ROUTE_LIMIT_EXCEEDED);
Expand All @@ -223,6 +268,8 @@ synchronized public AllRoutesInfo createRoute(Long univId, CreateRoutesReqDTO re
nodeRepository.saveAll(nodesForSave);
routeRepository.saveAll(routes);

redisService.deleteData(univId.toString());

return routeCalculator.assembleRoutes(routes);
}
}
Loading

0 comments on commit 884e2a7

Please sign in to comment.