동기 방식과 비동기 방식을 비교하고, 이벤트 방식으로 무엇을 개선할 수 있는지 살펴본다.
전체 코드: https://github.com/ji-jjang/Learning/tree/main/Practice/Event
cf) 동기적으로 실행 중인 스레드가 Block 되었다면 다른 HTTP 요청을 처리할 수 있을까?
스프링 톰캣(WAS)는 멀티 스레드로 동작하기 때문에 현재 요청이 Block 상태여도, 다른 요청은 별도의 스레드로 실행된다는 점을 주의하자!
사용자에게 빠른 응답을 제공하고, 외부서비스와의 강한 결합을 해소하기 위해 비동기 프로그래밍이 필요하다는 걸 알 수 있다.
@EnableAsync
를 추가하고, 비동기적으로 실행할 메서드 위에 @Async
를 추가하면 된다.@Configuration
@EnableAsync
public class Config { ...}
@Async
public User register(ReqCreateUser req) throws InterruptedException {
User savedUser = userRepository.save(new User(req.name(), req.email(), req.password(), req.recommender()));
emailService.sendEmailToNewUser(req.email());
couponService.sendCouponToNewUser(req.email());
pointService.chargePoint(req.recommender());
return savedUser;
}
@Async
public void sendCouponToNewUser(String email) throws InterruptedException { ... }
@Async
public void sendCouponToNewUser(String email) throws InterruptedException { ... }
@Async
public void chargePoint(String referrer) throws InterruptedException { ... }
// 1. Spring이 비동기 메소드를 프록시 객체로 감싸도록 함
public @interface EnableAsync {
...
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
...
}
// 2. 비동기 처리 설정을 하는 ProxyAsyncConfiguration 객체 생성
public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {
@NonNull
public String[] selectImports(AdviceMode adviceMode) {
case PROXY -> var10000 = new String[]{ProxyAsyncConfiguration.class.getName()};
...
}
// 3. @Async 어노테이션이 붙은 메소드를 프록시로 변환
public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
...
AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
bpp.configure(this.executor, this.exceptionHandler); // 실행자 설정
bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass"));
...
}
}
CompletableFuture.allOf()
를 이용해 특정 비동기 작업을 선택할 수 있고 join()
메서드를 통해 비동기 작업의 결과를 받을 수 있다.CompletableFuture
는 다양한 메서드를 지원한다.supplyAsync()
: 비동기 작업을 수행하고 결과를 반환runAsync()
: 비동기 작업을 수행하지만 결과를 반환하지 않음thenApply()
: 비동기 작업이 완료된 후 그 결과를 다른 작업에 적용thenAccept()
: 비동기 작업이 완료된 후 그 결과를 소비thenRun()
: 비동기 작업이 완료된 후 실행할 작업을 지정thenCombine()
: 두 개의 비동기 작업을 조합하여 결과를 처리exceptionally()
: 예외가 발생했을 때 처리할 작업을 지정handle()
: 비동기 작업의 성공 또는 실패를 처리allOf()
: 여러 비동기 작업을 모두 완료할 때까지 대기anyOf()
: 여러 비동기 작업 중 하나가 완료될 때까지 대기public User register(ReqCreateUser req) throws InterruptedException {
User savedUser = userRepository.save(new User(req.name(), req.email(), req.password(), req.recommender()));
CompletableFuture<Void> emailFuture = emailService.sendEmailToNewUser(req.email());
CompletableFuture<Void> couponFuture = couponService.sendCouponToNewUser(req.email());
CompletableFuture<Void> pointFuture = pointService.chargePoint(req.recommender());
CompletableFuture.allOf(emailFuture, couponFuture, pointFuture).join();
return savedUser;
}
서비스의 단일 책임 원칙 그리고 느슨한 결합을 위해 이벤트 프로그래밍이 필요하다는 것을 알 수 있다!
@Bean(name = "applicationEventMulticaster")
public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
return eventMulticaster;
}
public record UserCreatedEvent(
String email,
String recommender) { }
@Transactional
public User register(ReqCreateUser req) throws InterruptedException {
User savedUser = userRepository.save(new User(req.name(), req.email(), req.password(), req.recommender()));
eventPublisher.publishEvent(new UserCreatedEvent(req.email(), req.recommender()));
return savedUser;
}
@Service
@Slf4j
public class PointService {
@Async
@EventListener
public void chargePoint(UserCreatedEvent event) throws InterruptedException {
if (event.recommender() == null || event.recommender().isEmpty()) {
log.info("referrer is empty or null : {}", event.recommender());
return;
}
log.info("charge point");
for (int i = 0; i < 2; i++) {
log.info("charging point...");
Thread.sleep(1000);
}
}
}