package com.psj.itembrowser.mail;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import javax.persistence.EntityManager;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.psj.itembrowser.cart.domain.entity.CartEntity;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class MailMockService {
private final EntityManager em;
@Transactional(readOnly = true)
public void dbSave() {
long successCount = 0L;
long failCount = 0L;
for (int i = 0; i < 100; i++) {
try {
CompletableFuture<Boolean> result = sendMockMail();
result.get();
successCount++;
} catch (InterruptedException | ExecutionException e) {
failCount++;
}
}
log.info("성공 횟수 : {}", successCount);
log.info("실패 횟수 : {}", failCount);
log.info("메일 보내기 작업이 성공한 친구들만 별도 db에 기록작업을 수행한다.");
}
@Async("mailSenderExecutor")
public CompletableFuture<Boolean> sendMockMail() throws InterruptedException {
log.info(Thread.currentThread().getName() + " - 메일을 보내는 중입니다.");
Thread.sleep(1000);
// CompletableFuture.completedFuture를 사용하여 즉시 완료된 Future를 반환합니다.
return CompletableFuture.completedFuture(true);
}
}
package com.psj.itembrowser.mail;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RestController
@RequiredArgsConstructor
@Slf4j
public class MailMockController {
private final MailMockService mailMockService;
@GetMapping("/mail")
public void mail() {
mailMockService.dbSave();
}
}
package com.psj.itembrowser.security.common.config.mail;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class MailConfig implements AsyncConfigurer {
@Override
@Bean(name = "mailSenderExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("mailSender-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//executor.initialize();
return executor;
}
}
💡 ❗❗❗❗❗주의❗❗❗❗❗
initialize()
메서드의 경우
- getAsyncExecutor 가 빈으로 등록되지 않은 경우에 별도로 추가해줘야하는 메서드입니다.
- 스프링에서 빈으로 등록된 경우 별도의 초기화과정은 필요하지 않습니다
- 스프링 자체에서 별도 초기화를 자동으로 관리해줍니다.
@Async
내부 동작 관련@EnableAsync
이 활성화된 경우 AsyncAnnotationBeanPostProcessor
를 등록하게 되는데@Async
동작을 프록시로 감싸버리는 것이다 )package com.psj.itembrowser.mail;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class MailMockService2 {
private final MailMockService mailMockService;
@Transactional(readOnly = true)
public CompletableFuture<Void> dbSave() {
// 비동기 작업을 위한 CompletableFuture 리스트를 생성합니다.
List<CompletableFuture<Boolean>> futures = IntStream.range(0, 100)
.mapToObj(i -> mailMockService.sendMockMail())
.collect(Collectors.toList());
// 모든 CompletableFuture가 완료될 때까지 기다립니다.
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> {
// 모든 작업이 완료된 후, 성공 및 실패 카운트를 계산합니다.
long successCount = futures.stream()
.filter(CompletableFuture::join) // join()을 사용하여 결과를 가져옵니다.
.count();
long failCount = futures.size() - successCount;
// 결과를 로깅합니다.
log.info("성공 횟수 : {}", successCount);
log.info("실패 횟수 : {}", failCount);
log.info("메일 보내기 작업이 성공한 친구들만 별도 db에 기록작업을 수행한다.");
return null; // Void 타입의 CompletableFuture를 반환하기 위해 null을 사용합니다.
});
}
}
package com.psj.itembrowser.mail;
import java.util.concurrent.CompletableFuture;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class MailMockService {
@Async("mailSenderExecutor")
public CompletableFuture<Boolean> sendMockMail() {
log.info(Thread.currentThread().getName() + " - 메일을 보내는 중입니다.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// CompletableFuture.completedFuture를 사용하여 즉시 완료된 Future를 반환합니다.
return CompletableFuture.completedFuture(true);
}
}
@RestController
@RequiredArgsConstructor
@Slf4j
public class MailMockController {
private final MailMockService2 mailMockService;
@GetMapping("/mail")
public void mail() {
long start = System.currentTimeMillis();
mailMockService.dbSave().thenRun(() -> {
long end = System.currentTimeMillis();
log.info("메일 보내기 작업이 완료되었습니다. 소요시간 : {}", end - start);
});
}
}
💡 다만, 주의해야하는 점이 있다.
- HTTP 별도 반환이 필요한 경우에는
CompletableFuture
를 HTTP 응답으로 직접 반환하거나, 다른 방법을 고려해야한다.
AsyncAnnotationBeanPostProcessor (Spring Framework 5.3.3 API)