Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3a96ba0
feat: MDC 필터 추가
Dimo-2562 Feb 22, 2026
be6de7a
feat: 인증된 사용자는 JwtAuthenticationFilter에서 userId 갱신
Dimo-2562 Feb 22, 2026
38d529c
chore: logback-spring.xml 작성
Dimo-2562 Feb 22, 2026
a7a53db
chore: shutdown Hook 설정 추가
Dimo-2562 Feb 22, 2026
863893b
chore: root에서 상속하여 중복을 제거
Dimo-2562 Feb 22, 2026
795e8c4
chore: 크롤링 로그는 별도로 분리
Dimo-2562 Feb 22, 2026
a85f8f3
chore: 에러 로그는 60일로 보관기간 증가
Dimo-2562 Feb 22, 2026
754b5ed
chore: Actuator 로그는 안찍히도록 WARN 레벨로 낮춤
Dimo-2562 Feb 22, 2026
c85655a
refactor: 상수 별도 클래스로 분리
Dimo-2562 Feb 22, 2026
4ab6a42
improve: executor에 TaskDecorator로 MDC 복사-전파 처리
Dimo-2562 Feb 22, 2026
b8286a6
refactor: searchAsyncExecutor를 search 패키지 아래로 이동
Dimo-2562 Feb 22, 2026
246636f
improve: 검색 executor에 TaskDecorator로 MDC 복사-전파 처리
Dimo-2562 Feb 22, 2026
7776f4c
feat: Async 전용 Executor 설정 추가
Dimo-2562 Feb 22, 2026
fd624a9
feat: Recommendation 전용 Executor 추가
Dimo-2562 Feb 22, 2026
1b8d348
improve: Spring Batch는 별도의 스레드이므로 JobExecutionListener 클래스에 별도로 설정
Dimo-2562 Feb 22, 2026
50d7195
improve: WARN 로그에 따라 DelayingShutdownHook → DefaultShutdownHook으로 클래스…
Dimo-2562 Feb 22, 2026
06a41a7
refactor: local 환경에서는 파일이 생성되지 않도록 appender를 dev 블록 안으로 이동
Dimo-2562 Feb 22, 2026
19e6c61
feat: MDC 필터에 API 소요시간 체크로직 추가
Dimo-2562 Feb 22, 2026
8ae0654
fix: RecommendationService 필드 추가에 따른 테스트 코드 생성자 인자 업데이트
Dimo-2562 Feb 22, 2026
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ stop-dev-tunnel.sh
/CLAUDE.md
/.claude

### logs ###
logs/

### env ###
.env

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.techfork.domain.recommendation.config;

import com.techfork.global.filter.MdcTaskDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
public class RecommendationConfig {

/**
* 추천 생성 전용 executor.
* 검색(SearchConfig)과 스레드풀을 분리하여 추천 배치 작업이 실시간 검색에 영향을 주지 않도록 한다.
*/
@Bean(name = "recommendationAsyncExecutor")
public Executor recommendationAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(20);
executor.setThreadNamePrefix("recommendation-");
executor.setTaskDecorator(new MdcTaskDecorator());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.techfork.global.util.VectorUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
Expand All @@ -33,6 +34,7 @@
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;

/**
Expand All @@ -55,6 +57,8 @@ public class LlmRecommendationService implements RecommendationService {
private final TimeDecayStrategy timeDecayStrategy;
private final RecommendationProperties properties;
private final VectorQueryBuilder vectorQueryBuilder;
@Qualifier("recommendationAsyncExecutor")
private final Executor recommendationAsyncExecutor;

private static final String POSTS_INDEX = "posts";
private static final String TITLE_EMBEDDING_FIELD = "titleEmbedding";
Expand Down Expand Up @@ -154,7 +158,7 @@ private List<MmrCandidate> searchCandidates(float[] userProfileVector, User user
log.error("kNN 검색 실패", e);
return Collections.emptyList();
}
});
}, recommendationAsyncExecutor);

CompletableFuture<List<Hit<PostDocument>>> keywordSearchFuture = CompletableFuture.supplyAsync(() -> {
// 키워드가 없으면 BM25 검색 생략
Expand All @@ -178,7 +182,7 @@ private List<MmrCandidate> searchCandidates(float[] userProfileVector, User user
log.error("BM25 검색 실패", e);
return Collections.emptyList();
}
});
}, recommendationAsyncExecutor);

// 4. 두 검색 완료 대기
CompletableFuture<Void> allSearches = CompletableFuture.allOf(vectorSearchFuture, keywordSearchFuture);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
package com.techfork.global.config;
package com.techfork.domain.search.config;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import com.techfork.global.filter.MdcTaskDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
public class SearchConfig {

@Bean(name = "searchAsyncExecutor")
public Executor searchAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setCorePoolSize(20);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setTaskDecorator(new MdcTaskDecorator());
executor.setThreadNamePrefix("SearchExec-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

return executor;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.techfork.domain.source.batch.RssToPostProcessor;
import com.techfork.domain.source.dto.RssFeedItem;
import com.techfork.domain.source.listener.RssCrawlingJobListener;
import com.techfork.global.filter.MdcTaskDecorator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
Expand Down Expand Up @@ -168,6 +169,7 @@ public TaskExecutor summaryTaskExecutor() {
executor.setMaxPoolSize(2);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("summary-");
executor.setTaskDecorator(new MdcTaskDecorator());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
Expand All @@ -181,6 +183,7 @@ public TaskExecutor embeddingTaskExecutor() {
executor.setMaxPoolSize(20);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("embedding-");
executor.setTaskDecorator(new MdcTaskDecorator());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.techfork.domain.source.listener;

import com.techfork.domain.source.service.WebhookNotificationService;
import com.techfork.global.constant.MdcKey;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
Expand Down Expand Up @@ -30,9 +32,10 @@ public class RssCrawlingJobListener implements JobExecutionListener {

@Override
public void beforeJob(JobExecution jobExecution) {
Long jobExecutionId = jobExecution.getId();
MDC.put(MdcKey.REQUEST_ID, "batch-" + jobExecution.getId());
MDC.put(MdcKey.USER_ID, "system");
log.info("RSS crawling job started: jobExecutionId={}, startTime={}",
jobExecutionId, jobExecution.getStartTime());
jobExecution.getId(), jobExecution.getStartTime());
}

@Override
Expand All @@ -44,6 +47,7 @@ public void afterJob(JobExecution jobExecution) {
} else {
handleJobFailure(jobExecution, "Job failed with status: " + batchStatus);
}
MDC.clear();
}

/**
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/com/techfork/global/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.techfork.global.config;

import com.techfork.global.filter.MdcTaskDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig {

/**
* @Async 기본 executor.
* MdcTaskDecorator로 부모 스레드의 MDC 컨텍스트를 자식 스레드에 복사한다.
*/
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("async-");
executor.setTaskDecorator(new MdcTaskDecorator());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/techfork/global/constant/MdcKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.techfork.global.constant;

public final class MdcKey {
private MdcKey() {}

public static final String REQUEST_ID = "requestId";
public static final String USER_ID = "userId";
public static final String METHOD = "method";
public static final String URI = "uri";
public static final String CLIENT_IP = "clientIp";
}
72 changes: 72 additions & 0 deletions src/main/java/com/techfork/global/filter/MdcLoggingFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.techfork.global.filter;

import com.techfork.global.constant.MdcKey;
import jakarta.servlet.FilterChain;
import lombok.extern.slf4j.Slf4j;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.UUID;

/**
* HTTP 요청마다 MDC에 트레이싱 컨텍스트를 설정하는 필터.
* 요청 종료 시 MDC를 정리하여 스레드 풀 재사용 시 누수를 방지한다.
* <p>
* MDC 필드:
* - requestId: 요청 단위 고유 ID (UUID)
* - userId: 인증된 사용자 ID (비인증 요청은 "anonymous")
* - method: HTTP 메서드
* - uri: 요청 URI
* - clientIp: 클라이언트 IP (X-Forwarded-For 우선)
*/
@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MdcLoggingFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
try {
MDC.put(MdcKey.REQUEST_ID, UUID.randomUUID().toString().replace("-", "").substring(0, 12));
MDC.put(MdcKey.METHOD, request.getMethod());
MDC.put(MdcKey.URI, request.getRequestURI());
MDC.put(MdcKey.CLIENT_IP, resolveClientIp(request));
MDC.put(MdcKey.USER_ID, "anonymous");

filterChain.doFilter(request, response);

log.info("{} {} {} {}ms",
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
System.currentTimeMillis() - startTime);
} finally {
MDC.clear();
}
}

private String resolveClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (isValid(ip)) {
return ip.split(",")[0].trim();
}
ip = request.getHeader("X-Real-IP");
if (isValid(ip)) {
return ip;
}
return request.getRemoteAddr();
}

private boolean isValid(String ip) {
return ip != null && !ip.isBlank() && !"unknown".equalsIgnoreCase(ip);
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/techfork/global/filter/MdcTaskDecorator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.techfork.global.filter;

import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;

import java.util.Map;

/**
* 부모 스레드의 MDC 컨텍스트를 자식 스레드로 복사하는 TaskDecorator.
* ThreadPoolTaskExecutor에 등록하여 @Async, AsyncItemProcessor 등에서 MDC가 전파되도록 한다.
*/
public class MdcTaskDecorator implements TaskDecorator {

@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
return () -> {
try {
if (mdcContext != null) {
MDC.setContextMap(mdcContext);
}
runnable.run();
} finally {
MDC.clear();
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.techfork.domain.user.entity.User;
import com.techfork.domain.user.repository.UserRepository;
import com.techfork.global.constant.Constants;
import com.techfork.global.constant.MdcKey;
import com.techfork.global.exception.GeneralException;
import com.techfork.global.security.jwt.JwtUtil;
import com.techfork.global.security.oauth.UserPrincipal;
Expand All @@ -14,6 +15,7 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
Expand Down Expand Up @@ -66,6 +68,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
securityContext.setAuthentication(authentication);
SecurityContextHolder.setContext(securityContext);

MDC.put(MdcKey.USER_ID, String.valueOf(userId));
log.debug("Set authentication for user: {}", userId);
}
} catch (Exception e) {
Expand Down
7 changes: 2 additions & 5 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,8 @@ spring:
uris: http://elasticsearch:9200

logging:
level:
com.techfork: INFO
org.springframework.batch: INFO
org.hibernate.SQL: INFO
org.hibernate.type.descriptor.sql.BasicBinder: WARN
file:
path: /app/logs

apple:
private-key-path: /app/keys/AuthKey_${APPLE_KEY_ID}.p8
7 changes: 0 additions & 7 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,6 @@ spring:
elasticsearch:
uris: http://elasticsearch:9200

logging:
level:
com.techfork: DEBUG
org.springframework.batch: INFO
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE

# Actuator 설정 - Resilience4j 모니터링
management:
endpoints:
Expand Down
Loading