본문 바로가기
Spring/Spring-detail

Spring Boot로 구현하는 이벤트 기반 비동기 처리 (입문)

by YoonJong 2025. 4. 3.
728x90

이벤트 기반 개발 : 시스템이 이벤트를 발생시키고, 이를 구독(Listener) 하는 컴포넌트가 반응하는 방식

비유하자면 카페에서 주문(이벤트)을 하면 바리스타가 커피를 만드는 것처럼, 작업이 분리되어 처리하는 방식이다.

 

장점

- 느슨한 결합 : 이벤트 발행자와 소비자가 독립적

- 확정성 : 비동기 처리로 부하 분산

- 반응성 : 실시간 처리 가능

- 예시 : 계좌 이체 후 이메일 알림, 결제 완료 후 재고 감소 등

 

Spring에서의 이벤트 처리

도구 

- ApplicationEvent : 이벤트 정의

- AppliactionEventPublisher : 이벤트 발행

- @EventListener : 이벤트 수신

- 비동기 : @Async로 이벤트 처리 비동기화

 

 

Executor

- Java 에서 비동기 작업을 실행하기 위한 인터페이스

- 단일 메서드 execute(Runnable)을 정의하며, 작업을 스레드에 위임

비유하면 Executor는 공장 관리자이며, 작업자(스레드)를 직접 관리하지 않고, 작업표(Runnable)을 주면 알아서 처리하라고 맡기는 것. 

- 구현체

    ㄴ ThreadPoolTaskExecutor : 스레드 풀 기반, 커스텀 가능 > 해당 예제에서 사용

    ㄴ ScheduledExecutorService : 주기적 작업 (예시 : 매일 환율 갱신)

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(2);

 

 

--

비동기 설정 클래스 ( main 에다가 @EnableAsync 를 붙여 사용해도 되지만, 조금 더 효율적으로 사용이 가능 )

@Configuration
@EnableAsync // 비동기 기능 활성화 (@Async 사용 가능)
public class AsyncConfig {

	@Bean(name = "taskExecutor") // 비동기 작업을 위한 커스텀 Executor 빈 등록
    public Executor taskExecutor() {
        // ThreadPoolTaskExecutor: 스레드 풀 기반 비동기 실행기
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        // 기본 스레드 수: 5개 (평소 유지되는 스레드)
        executor.setCorePoolSize(5);

        // 최대 스레드 수: 10개 (큐가 가득 차면 증가)
        executor.setMaxPoolSize(10);

        // 대기 큐 크기: 100개 (초과 요청 대기열)
        executor.setQueueCapacity(100);

        // 스레드 이름 접두사: 로그에서 스레드 구분 용이
        executor.setThreadNamePrefix("AsyncThread-");

        // 작업 거부 정책: 큐와 풀이 가득 찼을 때
        executor.setRejectedExecutionHandler((r, e) -> {
            System.err.println("작업 거부: " + r + ", 풀 상태: " + e.getActiveCount());
        });

        // 초기화: 설정 적용 및 실행 준비
        executor.initialize();

        return executor; // Spring 컨텍스트에 등록
    }
}

 

 

TransferEvent

@Getter
public class TransferEvent extends ApplicationEvent {

    private final String fromAccount;
    private final String toAccount;
    private final double amount;

    public TransferEvent(Object source,
                         String fromAccount,
                         String toAccount,
                         double amount) {
        super(source);
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
    }

}

 

TransferService

@Service
public class TransferService {
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public void transfer(String fromAccount, String toAccount, double amount) {
        System.out.println("이체 시작: " + fromAccount + " -> " + toAccount + ", 금액: " + amount);
        eventPublisher.publishEvent(new TransferEvent(this, fromAccount, toAccount, amount));
        System.out.println("이체 완료");
    }
}

 

NotificationListener

@Slf4j
@Component
public class NotificationListener {

    // 이벤트 리스너: 이체 이벤트 처리
    @EventListener      // TransferEvent 발생 시 호출
    @Async              // 비동기 실행 (AsyncConfig의 taskExecutor 사용)
    public void handleTransferEvent(TransferEvent event) {
        try {
            Thread.sleep(2000); // 비동기 동작 확인용 지연
            log.info(
                "알림 전송: {}가 {}에게 {}원 보냄 (스레드: {})",
                event.getFromAccount(),
                event.getToAccount(),
                event.getAmount(),
                Thread.currentThread().getName()
            );
        } catch (InterruptedException e) {
            log.error("알림 처리 중 오류: {}", e.getMessage(), e); // 예외 로깅
            Thread.currentThread().interrupt(); // 스레드 상태 복구
        }
    }
}

 

TransferController

@RestController
@RequiredArgsConstructor
public class TransferController {

    private final TransferService transferService;

    @PostMapping("/transfer")
    public String performTransfer(@RequestParam String from,
                                  @RequestParam String to,
                                  @RequestParam double amount) {
        transferService.transfer(from, to, amount);
        return "이체 요청 완료";
    }
}

 

 

연속으로 실행했을 경우 결과값

728x90

댓글