Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
20 changes: 15 additions & 5 deletions uniro_backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ configurations {
compileOnly {
extendsFrom annotationProcessor
}

all {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
}

repositories {
Expand Down Expand Up @@ -88,12 +92,18 @@ 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'
// cache
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'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'

// fastjson
implementation 'com.alibaba.fastjson2:fastjson2-extension-spring6:2.0.56'

// log4j2
implementation "org.springframework.boot:spring-boot-starter-log4j2" // Spring Boot Log4j2
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" // Jackson Dataforamt yaml
implementation "com.lmax:disruptor:3.4.4"

}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.softeer5.uniro_backend.admin.service;

import static com.softeer5.uniro_backend.common.constant.UniroConst.*;
import static com.softeer5.uniro_backend.common.error.ErrorCode.*;

import com.softeer5.uniro_backend.admin.annotation.DisableAudit;
Expand All @@ -13,7 +12,6 @@
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.external.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 All @@ -22,6 +20,7 @@
import com.softeer5.uniro_backend.map.entity.Route;
import com.softeer5.uniro_backend.map.repository.NodeRepository;
import com.softeer5.uniro_backend.map.repository.RouteRepository;
import com.softeer5.uniro_backend.map.service.RouteCacheService;
import com.softeer5.uniro_backend.map.service.RouteCalculator;
import com.softeer5.uniro_backend.univ.entity.Univ;
import com.softeer5.uniro_backend.univ.repository.UnivRepository;
Expand All @@ -47,7 +46,7 @@ public class AdminService {

private final RouteCalculator routeCalculator;

private final RedisService redisService;
private final RouteCacheService cacheService;

public List<RevInfoDTO> getAllRevInfo(Long univId){

Expand Down Expand Up @@ -114,8 +113,7 @@ public void rollbackRev(Long univId, Long versionId){
}
}

int routeCount = routes.size();
redisService.deleteRoutesData(univId.toString(), routeCount / STREAM_FETCH_SIZE + 1);
cacheService.deleteFetchSizeAndLightRoutesByUnivId(univId);
}

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

import java.util.concurrent.TimeUnit;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.github.benmanes.caffeine.cache.Caffeine;

@Configuration
@EnableCaching
public class CaffeineConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("lightRoutes", "tmp"); // 여러 개의 캐시 네임 설정 가능
cacheManager.setCaffeine(caffeineConfig());
return cacheManager;
}

public Caffeine<Object, Object> caffeineConfig() {
return Caffeine.newBuilder()
.expireAfterWrite(12, TimeUnit.HOURS) // 12시간 후 캐시 만료
.maximumSize(3000) // 최대 3000개 저장
.recordStats(); // 캐시 통계 활성화
Comment on lines +23 to +27
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

여기서 최대 3000개란 이중에 무엇을 의미하는지 궁금합니다!

  1. 학교 3000개
  2. lightRoute 객체 3000개
  3. lightRoute 2500개에 해당하는 key 3000개

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

3번을 의미합니다 ! 이 부분도 어떻게 생각하시는지 궁금합니다 !

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

최대 2500 * 3000 = 750만 개의 데이터는 1GB 이하니깐 메모리 관점에서는 괜찮다고 생각합니다. (현재 8GB 서버 기준)

그런데 전국 약 300개 대학의 길 데이터를 모두 합쳐도 750만개가 안될것 같습니다.
그 말은 곧 전국의 모든 데학교 데이터를 캐싱할 수 있다는 말인데, 모든 데이터를 캐싱할 수 있다면 TTL이 꼭 필요한가에 대해 논의해보면 좋을 것 같습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

음 안그래도 저도 며칠전부터 그런 의문이 있었어서 공감이 되네요!
더 나아가 데이터가 수정될 때도, "캐시를 eviction 하지않고 수정할 때마다 업데이트 해주면 TTL은 의미가 없는게 아닌가?" 생각을 했습니다.

그래서 관련 자료들을 찾아봤는데여! 설득력이 있던 얘기는 "불필요한 데이터가 메모리에 너무 많이 올라가 있다." 였습니다. 정말 많은 사용자가 들어오게 된다면, 그에 따라 많은 메모리 공간이 필요하게 됩니다.

만약 1달 전에 사용했던 데이터도 로컬캐시에 올라와있으면 기본적으로 메모리 사용량이 높아져 TPS가 떨어지거나 OOM 발생 확률이 높아진다고 생각했습니다.
하지만 반대로 TTL이 있다면, 정말 "핫한" 데이터만 동적으로 메모리에 올라와있기 때문에 많은 사용자가 들어와도 유연하게 대처 && 조금 더 많은 사용자를 처리할 "수"(가능성이 높아진다) 있다고 생각했습니다.

Redis 와 같은 리모트 캐시에도 유효하다고 생각했습니다. 비슷한 이유로 필요없는 데이터가 메모리에 항상 올라와있다는 것이 불필요하고, 시스템의 여러 매트릭을 불필요하게 높이는 요인이라고 생각했습니다!

반대로 TTL를 도입하면 tradeoff로 말씀주신 것처럼 DB로의 접근은 늘어날 수 밖에 없습니다. 또한, TTL이 굉장히 짧게 설정되어 있다면 오히려 그 부분이 TPS를 떨어뜨리는 요인이 될 수도 있겠죠,,

이게 참 애매한게 실제 사용자 트래픽이 있다면, 그 트래픽을 기준으로 의사결정을 할 수 있을 것 같은데 그러지 못하는게 아쉽네요..
그래서 저도 댓글 적다가 생각이 든건데, 제가 한 번 실제 테스트를 해보면서 수치 결과를 통해 두 방식을 비교해보는 것도 좋겠다는 생각도 드네요!

안그래도 해당 내용으로 얘기를 하고 싶었는데 좋은 질문 감사드리고, 현성님 의견이 궁금하네요..ㅎ

Copy link
Copy Markdown
Collaborator

@thdgustjd1 thdgustjd1 Mar 28, 2025

Choose a reason for hiding this comment

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

말씀해주신대로 확실히 TTL없이 할 경우 메모리낭비가 될 수 있을것 같습니다. 그렇다면 TTL을 적절하게 설정하는게 좋을 것 같네요!

아직 트래픽이 있지 않아서 정확하진 않지만, 서비스 특성상 오전8시-오후6시 사이에 호출량이 많을것으로 예상됩니다. (이 오전8시-오후6시를 피크타임이라고 하겠습니다.) TTL이 12시간일 경우 오늘 캐시에 올라간 데이터는 밤-새벽 사이에 만료되어 다음날 피크타임에 호출할 땐 다시 DB접근이 이루어질 것 같습니다. (핫 키도 밤-새벽 사이에 만료됨)

이러한 상황에서는 TTL을 36시간 이상으로 늘리거나, TTL 없이 최근 며칠간 접근되지 않은것을 지우거나 하는 방향으로 가면 좋을 것 같습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

그럼 일단 TTL을 36시간으로 설정하고, 추후에는 핫키에 대해서 동적으로 TTL을 갱신해보는 것을 고민해보겠습니다!

좋은 의견 감사합니다 :)

}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.softeer5.uniro_backend.common.config;

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

Expand Down Expand Up @@ -49,6 +50,22 @@ public void addInterceptors(InterceptorRegistry registry) {
.excludePathPatterns("/**", HttpMethod.OPTIONS.name())
.order(1); // JWT 이후에 실행되도록 설정
}

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
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);
converter.setFastJsonConfig(config);
converter.setDefaultCharset(StandardCharsets.UTF_8);
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM);
converter.setSupportedMediaTypes(supportedMediaTypes);
converters.add(0, converter);
}
}


Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.softeer5.uniro_backend.common.logging;

import static com.softeer5.uniro_backend.common.constant.UniroConst.*;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
Expand All @@ -8,21 +12,26 @@
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.log4j.Log4j2;
import lombok.extern.slf4j.Slf4j;

@Aspect
@Component
@Log4j2
@Slf4j
@Profile("!test")
public class ExecutionLoggingAop {
Logger asyncLogger = LoggerFactory.getLogger("async-logger");
Logger synclogger = LoggerFactory.getLogger(ExecutionLoggingAop.class);

private static final ThreadLocal<String> userIdThreadLocal = new ThreadLocal<>();

Expand All @@ -48,44 +57,41 @@ public Object logExecutionTrace(ProceedingJoinPoint pjp) throws Throwable {
}
HttpServletRequest request = null;
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes) {
request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
}

if(request==null){
if (request == null) {
return pjp.proceed();
}
log.info("✅ [ userId = {} Start] [Call Method] {}: {}", userId, request.getMethod(), task);
asyncLogger.info("✅ [ userId = {} Start] [Call Method] {}: {}", userId, request.getMethod(), task);

try{
try {
if (isController) {
logParameters(pjp.getArgs());
}
}
catch (Exception e){
} catch (Exception e) {
// 로깅 중에 발생한 에러는 무시하고 로깅을 계속 진행
log.error("🚨🚨🚨 [ userId = {} ] {} 메서드 파라미터 로깅 중 에러 발생 : {} 🚨🚨🚨", userId, task, e.getMessage());
asyncLogger.error("🚨🚨🚨 [ userId = {} ] {} 메서드 파라미터 로깅 중 에러 발생 : {} 🚨🚨🚨", userId, task, e.getMessage());
}
log.info("");

StopWatch sw = new StopWatch();
sw.start();

Object result;
try {
result = pjp.proceed();
sw.stop();
asyncLogger.info("🚨 [ExecutionTime] {} --> {} (ms) [ userId = {} ] {} End\n", task, sw.getTotalTimeMillis(),
userId, className);
} catch (Exception e) {
log.warn("[ERROR] [ userId = {} ] {} 메서드 예외 발생 : {}", userId, task, e.getMessage());
asyncLogger.warn("[ERROR] [ userId = {} ] {} 메서드 예외 발생 : {}", userId, task, e.getMessage());
throw e;
} finally {
if (isController) {
if (isControllerOrService(target)) {
userIdThreadLocal.remove();
}
}

sw.stop();
log.info("[ExecutionTime] {} --> {} (ms)", task, sw.getTotalTimeMillis());
log.info("🚨 [ userId = {} ] {} End\n", userId, className);

return result;
}

Expand All @@ -94,13 +100,23 @@ private boolean isRestController(Object target) {
.anyMatch(RestController.class::isInstance);
}

private boolean isControllerOrService(Object target) {
boolean b = Arrays.stream(target.getClass().getDeclaredAnnotations())
.anyMatch(RestController.class::isInstance);

boolean b1 = Arrays.stream(target.getClass().getDeclaredAnnotations())
.anyMatch(Service.class::isInstance);

return b || b1;
}

private void logParameters(Object[] args) {
StringBuilder parametersLogMessage = new StringBuilder();

Arrays.stream(args)
.forEach(arg -> logDetail(arg, "[Parameter]", parametersLogMessage, 0));

log.info("\n{}", parametersLogMessage.toString());
asyncLogger.info("\n{}", parametersLogMessage);
}

private void logDetail(Object arg, String requestType, StringBuilder logMessage, int depth) {
Expand Down Expand Up @@ -154,13 +170,16 @@ private void logObjectFields(Object object, StringBuilder logMessage, int depth)
Object value = field.get(object);
logDetail(value, "[Field] " + field.getName(), logMessage, depth + 1);
} catch (IllegalAccessException e) {
logMessage.append(indent).append("[Field Access Error] Cannot access field: ").append(field.getName()).append("\n");
logMessage.append(indent)
.append("[Field Access Error] Cannot access field: ")
.append(field.getName())
.append("\n");
}
});
}

private void logHttpRequest(String userId) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();

// HTTP Request 메시지 출력 (RFC 2616 형식)
StringBuilder httpMessage = new StringBuilder();
Expand Down Expand Up @@ -199,7 +218,6 @@ private void logHttpRequest(String userId) {
}

// 요청 메시지 출력
log.info("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ New request");
log.info("[ userId = "+ userId + " ] HTTP Request: \n" + httpMessage);
asyncLogger.info("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ New request\n [ userId = {} ] HTTP Request: {} \n", userId, httpMessage);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,6 @@
@Tag(name = "간선 및 위험&주의 요소 관련 Api")
public interface MapApi {

@Operation(summary = "모든 지도(노드,루트) 조회")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "모든 지도 조회 성공"),
@ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content),
})
ResponseEntity<AllRoutesInfo> getAllRoutesAndNodes(@PathVariable("univId") Long univId);

@Operation(summary = "모든 지도(노드,루트) 조회 by stream")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "모든 지도 조회 성공"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,6 @@ public class MapController implements MapApi {
private final MapService mapService;
private final AdminService adminService;

@Override
@GetMapping("/{univId}/routes")
public ResponseEntity<AllRoutesInfo> getAllRoutesAndNodes(@PathVariable("univId") Long univId){
AllRoutesInfo allRoutes = mapService.getAllRoutes(univId);
return ResponseEntity.ok().body(allRoutes);
}

@Override
@GetMapping("/{univId}/routes/stream")
public ResponseEntity<AllRoutesInfo> getAllRoutesAndNodesStream(@PathVariable("univId") Long univId){
Expand Down
Loading