πŸ“‹ 문제 상황

헀어샡 관리 μ‹œμŠ€ν…œμ—μ„œ κ³ κ°λ“€μ—κ²Œ 단체 λ©”μ‹œμ§€λ₯Ό μ „μ†‘ν•˜λŠ” κΈ°λŠ₯을 κ°œλ°œν•˜λ˜ 쀑, κ³ λ―Όν•  뢀뢄이 μƒκ²ΌμŠ΅λ‹ˆλ‹€.

κΈ°μ‘΄ μ½”λ“œμ˜ 문제점

// ❌ λ¬Έμ œκ°€ μžˆλŠ” κΈ°μ‘΄ 방식
for(ShopMessageHistory history : historyList){
    processMessageAsync(history, request, taskId); // 각각 μƒˆ μŠ€λ ˆλ“œ 생성
}

문제점:

πŸ”§ ν•΄κ²° λ°©μ•ˆ: 배치 처리 λ„μž…

1단계: μŠ€λ ˆλ“œ ν’€ μ„€μ •

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "messageTaskExecutor")
    public TaskExecutor messageTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(3);        // κΈ°λ³Έ μŠ€λ ˆλ“œ 수
        executor.setMaxPoolSize(10);        // μ΅œλŒ€ μŠ€λ ˆλ“œ 수
        executor.setQueueCapacity(100);     // λŒ€κΈ° 큐 크기
        executor.setThreadNamePrefix("Message-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

2단계: 배치 처리 λ©”μ„œλ“œ κ΅¬ν˜„

@Service
public class MessageService {

    @Async("messageTaskExecutor")
    public void processAllMessagesAsync(List<ShopMessageHistory> historyList,
                                       MessageDTO request,
                                       String taskId,
                                       Long batchId) {
        log.info("πŸ“¦ [{}] 전체 λ©”μ‹œμ§€ 처리 μ‹œμž‘ - 총 {}건", taskId, historyList.size());

        int successCount = 0;
        int failCount = 0;

        for(ShopMessageHistory history : historyList) {
            try {
                boolean success = sendSingleMessage(history, request);

                if(success) {
                    successCount++;
                    updateMessageStatus(history, MessageStatus.SUCCESS);
                } else {
                    failCount++;
                    updateMessageStatus(history, MessageStatus.FAILED);
                }

                // API μ œν•œ λ°©μ§€λ₯Ό μœ„ν•œ λ”œλ ˆμ΄
                if((successCount + failCount) % 10 == 0) {
                    Thread.sleep(200); // 10κ±΄λ§ˆλ‹€ 0.2초 λŒ€κΈ°
                }

            } catch (Exception e) {
                failCount++;
                log.error("[{}] λ©”μ‹œμ§€ 전솑 μ‹€νŒ¨ - UserCode: {}, Error: {}",
                         taskId, history.getUserCode(), e.getMessage());
                updateMessageStatus(history, MessageStatus.FAILED);
            }
        }

        // 배치 κ²°κ³Ό μ—…λ°μ΄νŠΈ
        updateBatchResult(batchId, successCount, failCount);

        log.info("βœ… [{}] 전체 처리 μ™„λ£Œ - 성곡: {}건, μ‹€νŒ¨: {}건", taskId, successCount, failCount);
    }

    private void updateBatchResult(Long batchId, int successCount, int failCount) {
        messageSendBatchService.updateBatchResult(batchId, successCount, failCount);
    }
}

3단계: 메인 둜직 μˆ˜μ •

public MessageResponse sendMessageAsync(MessageDTO request) {
    String taskId = generateTaskId();
    log.info("πŸ“© [{}] λ©”μ‹œμ§€ μš”μ²­ μ ‘μˆ˜ - UserCode 수: {}", taskId, request.getTo().size());

    // 1. batch DB에 μ €μž₯
    MessageSendBatchDTO batchDTO = MessageSendBatchDTO.builder()
            .shopCode(request.getFrom())
            .sendType(request.getSendType())
            .subject(request.getSubject())
            .totalCount(request.getTo().size())
            .successCount(0)
            .failCount(0)
            .build();

    MessageSendBatchDTO createdBatchDTO = messageSendBatchService.createMessageBatch(batchDTO);

    // 2. history μ €μž₯
    List<ShopMessageHistory> historyList = new ArrayList<>();
    for(Integer userCode : request.getTo()){
        historyList.add(saveAsPending(userCode, request.getText(), createdBatchDTO));
    }

    // 3. PENDING으둜 응닡
    MessageResponse response = new MessageResponse(MessageStatus.PENDING.toString());

    // 4. βœ… 단일 비동기 μž‘μ—…μœΌλ‘œ λͺ¨λ“  λ©”μ‹œμ§€ 처리
    processAllMessagesAsync(historyList, request, taskId, createdBatchDTO.getId());

    log.info("⚑ [{}] PENDING 응닡 μ¦‰μ‹œ λ°˜ν™˜", taskId);
    return response;
}

πŸ› οΈ μ£Όμš” νŠΈλŸ¬λΈ”μŠˆνŒ… μΌ€μ΄μŠ€

1. OutOfMemoryError ν•΄κ²°

문제: λŒ€λŸ‰ λ©”μ‹œμ§€ 전솑 μ‹œ λ©”λͺ¨λ¦¬ λΆ€μ‘± μ—λŸ¬ λ°œμƒ

// ❌ λ¬Έμ œκ°€ λ˜λŠ” μ½”λ“œ
List<ShopMessageHistory> historyList = new ArrayList<>();
for(int i = 0; i < 10000; i++) {
    historyList.add(createHistory(userCodes.get(i)));
}