-
Notifications
You must be signed in to change notification settings - Fork 2
[refactor] 동시성 이슈 #202
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
[refactor] 동시성 이슈 #202
Changes from all commits
6bf63e6
ce8d8b9
76b60f0
f91da9c
5392572
a4760b8
3c91432
ba634f5
5871614
9bff600
12a3d3c
be58d45
6e27397
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,73 @@ | ||
| package com.pitchain.common.config; | ||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.slf4j.MDC; | ||
| import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.core.task.TaskDecorator; | ||
| import org.springframework.scheduling.annotation.AsyncConfigurer; | ||
| import org.springframework.scheduling.annotation.EnableAsync; | ||
| import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | ||
|
|
||
| import java.lang.reflect.Method; | ||
| import java.util.Map; | ||
| import java.util.concurrent.Executor; | ||
|
|
||
| @Configuration | ||
| @EnableAsync | ||
| public class AsyncConfig implements AsyncConfigurer { | ||
|
|
||
| @Bean | ||
| public Executor asyncTaskExecutor() { | ||
| ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); | ||
| //TODO - corePoolSize, maxPoolSize, queueCapacity는 서비스에 맞게 테스트를 통해 설정 | ||
| executor.setCorePoolSize(Runtime.getRuntime().availableProcessors()); | ||
| executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors()); | ||
| // executor.setQueueCapacity(); | ||
| executor.setTaskDecorator(new MdcTaskDecorator()); | ||
| executor.setThreadNamePrefix("async"); | ||
| executor.setWaitForTasksToCompleteOnShutdown(true); | ||
| executor.initialize(); | ||
|
|
||
| return executor; | ||
| } | ||
|
Comment on lines
+21
to
+34
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. 💡 Verification agent 🧩 Analysis chainRedis 조회수 업데이트 용도로 스레드 풀 설정을 검토해주세요. 현재 설정은
다음 스크립트로 현재 설정이 Redis 작업에 적합한지 확인해보세요: 🏁 Script executed: #!/bin/bash
# Redis 연결 및 응답 시간 테스트를 위한 설정 확인
# SpViewsService에서 사용하는 Redis 작업 패턴 확인
rg -A 10 -B 2 "redisHashRepository\.increment"
# 현재 시스템의 프로세서 수 확인
echo "Available processors: $(nproc)"
# 동시 Redis 연결 테스트 스크립트 생성 제안
echo "동시성 테스트를 위해 Redis 연결 풀과 스레드 풀 설정을 비교 검토하세요."Length of output: 2822 Redis I/O 특화 스레드 풀 설정 개선 필요 AsyncConfig.java(21–34)에서 현재 CPU 코어 수만큼 스레드를 생성하도록 되어 있는데, Redis 호출은 I/O 대기 시간이 크므로 아래와 같이 수정하세요.
예시 변경안: src/main/java/com/pitchain/common/config/AsyncConfig.java
@Bean
public Executor asyncTaskExecutor(
- /* 하드코딩된 값 */
- ) {
- ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
- executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
- executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
- // executor.setQueueCapacity();
+ @Value("${async.core-pool-size}") int corePoolSize,
+ @Value("${async.max-pool-size}") int maxPoolSize,
+ @Value("${async.queue-capacity}") int queueCapacity
+ ) {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(corePoolSize);
+ executor.setMaxPoolSize(maxPoolSize);
+ executor.setQueueCapacity(queueCapacity);
executor.setTaskDecorator(new MdcTaskDecorator());
executor.setThreadNamePrefix("async");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.initialize();
return executor;
}application.yml 예시: async:
core-pool-size: 8
max-pool-size: 16
queue-capacity: 500• 위 설정으로 Redis I/O 처리량과 메모리 사용을 균형 있게 관리하세요. 🤖 Prompt for AI Agents |
||
|
|
||
| @Override | ||
| public Executor getAsyncExecutor() { | ||
| return asyncTaskExecutor(); | ||
| } | ||
|
|
||
| @Override | ||
| public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { | ||
| return new AsyncExceptionHandler(); | ||
| } | ||
|
|
||
| @Slf4j | ||
| private static class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler { | ||
|
|
||
| @Override | ||
| public void handleUncaughtException(Throwable throwable, Method method, Object... params) { | ||
| log.error("비동기 실행 중 에러가 발생했습니다: ", throwable); | ||
| } | ||
| } | ||
|
|
||
| @Slf4j | ||
| private static class MdcTaskDecorator implements TaskDecorator { | ||
| @Override | ||
| public Runnable decorate(Runnable runnable) { | ||
| Map<String, String> contextMap = MDC.getCopyOfContextMap(); | ||
| return () -> { | ||
| try { | ||
| if (contextMap != null) { | ||
| MDC.setContextMap(contextMap); | ||
| } | ||
| runnable.run(); | ||
| } finally { | ||
| MDC.clear(); | ||
| } | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.pitchain.common.redis; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.redis.core.HashOperations; | ||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.data.redis.core.script.RedisScript; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class RedisHashRepository { | ||
|
|
||
| private final RedisTemplate<String, String> redisTemplate; | ||
| private final RedisScript<List> script; | ||
|
|
||
| public void increment(String key, String hashKey, Long value) { | ||
| redisTemplate.opsForHash().increment(key, hashKey, value); | ||
| } | ||
|
|
||
| public Map<String, String> findAll(String key) { | ||
| HashOperations<String, String, String> ops = redisTemplate.opsForHash(); | ||
| return ops.entries(key); | ||
| } | ||
|
|
||
| public List<String> getAndDeleteAll(String key) { | ||
| return redisTemplate.execute(script, Collections.singletonList(key)); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,82 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package com.pitchain.sp.application; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.pitchain.common.redis.RedisHashRepository; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.pitchain.sp.infrastucture.SpRepositoryCustom; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.pitchain.sp.infrastucture.dto.SpViewsDto; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.scheduling.annotation.Async; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.scheduling.annotation.Scheduled; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.ArrayList; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Slf4j | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Service | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class SpViewsService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final SpRepositoryCustom spRepositoryCustom; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final RedisHashRepository redisHashRepository; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final String SP_VIEW_REDIS_KEY = "spView"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Redis에 Sp 조회수 증가 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param spId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Async | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void updateSpView(Long spId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| redisHashRepository.increment(SP_VIEW_REDIS_KEY, String.valueOf(spId), 1L); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Redis에서 DB로 Sp 조회수 업데이트 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Transactional | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void updateSpViews() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<String> spViewsResult = redisHashRepository.getAndDeleteAll(SP_VIEW_REDIS_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<SpViewsDto> spViewsDtoList = parseResult(spViewsResult); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (SpViewsDto spViewsDto : spViewsDtoList) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spRepositoryCustom.updateSpView(spViewsDto.spId(), spViewsDto.views()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 1분마다 Redis에서 DB로 Sp 조회수 업데이트하는 작업 수행 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Scheduled(cron = "0 */1 * * * *") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void runUpdateSpViews() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updateSpViews(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.error("Scheduling task [runUpdateSpViews] failed", e); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Redis에서 가져온 Sp 조회수 String 리스트를 Dto 리스트로 파싱 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param List<String> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @return List<SpViewsDto> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private List<SpViewsDto> parseResult(List<String> spViewsStringList) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (spViewsStringList.size() % 2 != 0){ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.error("spViewsStringList 개수가 올바르지 않습니다."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new IllegalArgumentException("spViewsStringList 개수가 올바르지 않습니다."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<SpViewsDto> spViewsDtoList = new ArrayList<>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (int i = 0; i < spViewsStringList.size(); i += 2) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Long spId = Long.parseLong(spViewsStringList.get(i)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Long views = Long.parseLong(spViewsStringList.get(i + 1)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SpViewsDto spViewsDto = new SpViewsDto(spId, views); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spViewsDtoList.add(spViewsDto); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return spViewsDtoList; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+65
to
+81
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. 🛠️ Refactor suggestion 파싱 로직에 예외 처리 개선 필요 기본적인 검증은 있지만 private List<SpViewsDto> parseResult(List<String> spViewsStringList) {
if (spViewsStringList.size() % 2 != 0){
log.error("spViewsStringList 개수가 올바르지 않습니다.");
throw new IllegalArgumentException("spViewsStringList 개수가 올바르지 않습니다.");
}
List<SpViewsDto> spViewsDtoList = new ArrayList<>();
for (int i = 0; i < spViewsStringList.size(); i += 2) {
- Long spId = Long.parseLong(spViewsStringList.get(i));
- Long views = Long.parseLong(spViewsStringList.get(i + 1));
+ try {
+ Long spId = Long.parseLong(spViewsStringList.get(i));
+ Long views = Long.parseLong(spViewsStringList.get(i + 1));
+
+ SpViewsDto spViewsDto = new SpViewsDto(spId, views);
+ spViewsDtoList.add(spViewsDto);
+ } catch (NumberFormatException e) {
+ log.error("Invalid number format in Redis data: spId={}, views={}",
+ spViewsStringList.get(i), spViewsStringList.get(i + 1), e);
+ // 잘못된 데이터는 건너뛰고 계속 처리
+ }
- SpViewsDto spViewsDto = new SpViewsDto(spId, views);
- spViewsDtoList.add(spViewsDto);
}
return spViewsDtoList;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package com.pitchain.sp.infrastucture.dto; | ||
|
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. 💡 Verification agent 🧩 Analysis chain패키지명 오타 확인 요청 패키지명에 "infrastucture"가 "infrastructure"의 오타로 보입니다. 프로젝트 전체에서 일관된 명명 규칙을 사용하고 있는지 확인해 주세요. 🏁 Script executed: #!/bin/bash
# 프로젝트 내 infrastructure/infrastucture 패키지명 사용 현황 확인
fd -t d "infrastucture"
fd -t d "infrastructure"Length of output: 771 패키지명 철자 통일 필요 프로젝트에 com.pitchain.* 아래에 영향 받는 오타 패키지 경로:
🤖 Prompt for AI Agents |
||
|
|
||
| public record SpViewsDto(Long spId, Long views) { | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| local data = redis.call('HGETALL', KEYS[1]) | ||
| redis.call('DEL', KEYS[1]) | ||
| return data |
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.
🛠️ Refactor suggestion
스레드 풀 설정을 재검토해주세요.
현재 설정에 몇 가지 개선이 필요합니다:
corePoolSize와maxPoolSize를 동일하게 설정하면 스레드 풀의 탄력성이 제한됩니다queueCapacity설정이 없으면 기본 무제한 큐가 사용되어 메모리 문제가 발생할 수 있습니다다음과 같은 설정을 고려해보세요:
📝 Committable suggestion
🤖 Prompt for AI Agents