11/25

졸용·2025년 11월 25일

TIL

목록 보기
121/144

🔹 Async란?

비동기 처리(asynchronous, async)는

지금 당장 결과가 올 때까지 기다리지 않고, 요청만 보내고 다음 작업을 계속 진행하는 방식을 의미한다.



🔹 동기 vs 비동기

동기(Sync)는 작업 A가 끝나야 작업 B를 실행할 수 있다.
비동기(Async)는 작업 A가 끝날 때까지 기다리지 않고 B를 바로 실행할 수 있다.



🔹 왜 필요한가? (비동기의 필요성)

정확한 근거를 들면 아래 3가지가 핵심이다.

(1) I/O 대기 시간이 긴 작업을 효율적으로 처리

  • 예: API 호출, DB 쿼리, 파일 업로드/다운로드
  • 이런 작업은 실제 연산 시간이 아니라 대기 시간이 대부분이기 때문에 스레드를 점유하고 기다릴 필요가 없음.

(2) 서버 자원 효율 증가

  • 요청이 많아질 때, 동기 방식이면 스레드 수만큼만 처리 가능
  • 비동기 방식이면 대기 시간 동안 스레드를 다른 업무로 넘겨서 더 많은 요청을 처리 가능

(3) 응답 시간 단축

  • 백그라운드에서 작업을 실행하도록 넘겨서 사용자 응답을 빠르게 제공


🔹 비동기 처리의 기본 작동 방식

요청 발생 → 작업을 별도 스레드에게 위임 → 메인 흐름은 즉시 다음 작업 수행

작업이 끝나면:

  • 콜백(callback)
  • Future/Promise
  • CompletableFuture
  • 이벤트(Listener)

같은 방식으로 결과를 알려줌.



🔹 Spring에서 비동기 처리

Spring에서는 @Async 를 가장 많이 사용한다고 한다.

🔸 @EnableAsync 추가

@Configuration
@EnableAsync
public class AsyncConfig {}

🔸 비동기 메서드 선언

@Service
public class MyService {

    @Async
    public void processAsync() {
        System.out.println("비동기 작업 실행");
    }
}

🔸 호출

myService.processAsync();
System.out.println("이 문장이 먼저 출력됨");

특징

  • 별도 스레드풀에서 실행
  • 반환 타입을 Future/CompletableFuture로 설정 가능

🔸 Spring 비동기 스레드풀 튜닝

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}

스레드 개수를 튜닝하면 고성능 비동기 구성 가능.



🔹 서비스 내부 비동기 @Async + 예외 처리

🔸 기본 설정

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }

    // @Async 메서드가 void 리턴일 때 예외 처리용
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> {
            // 여기서 로깅, 모니터링, 알림 등 처리
            log.error("Async error in method: {}", method.getName(), ex);
        };
    }
}

🔸 반환 타입별 예외 전파 규칙

void 리턴 @Async 방법

@Async
public void sendSlackLogAsync(String message) {
    // 예: 슬랙 전송 중 예외 발생
    throw new AppException(...);
}
  • 호출하는 쪽:
slackService.sendSlackLogAsync("msg");
System.out.println("이 문장은 바로 실행됨");
  • 여기서 AppException호출자에게 절대 가지 않음
  • 예외는 AsyncUncaughtExceptionHandler로 넘어감
    → “로깅/모니터링용 fire-and-forget 작업”에 적합한 패턴

MSA에서 “슬랙/이메일 로그 전송, 분석 로그 적재” 같은 부가 작업은
보통 이렇게 void @Async + 핸들러에서 로깅/알림 처리로 끝냄.


3. @Async + @Transactional 주의사항

중요 포인트 하나:

@Transactional
public void createOrder(...) {
    // DB insert
    orderRepository.save(...);

    // 비동기로 슬랙 전송
    notificationService.sendSlackAsync(...); // @Async
}
  • createOrder의 트랜잭션이 커밋되기 전에
    비동기 메서드 실행이 먼저 시작될 수 있음.
  • 비동기 메서드 내부에서 “방금 insert한 데이터”를 조회하면
    아직 커밋 전이라 조회 안 되는 문제가 생길 수 있음.

그래서 패턴은 보통:

  1. 트랜잭션 안에서 DB 작업만 끝까지 처리
  2. 그 후에 트랜잭션 밖에서 @Async 실행하거나,
  3. 아예 도메인 이벤트 발행 → 별도 Consumer에서 처리(Kafka 등)

MSA에서는 3번(도메인 이벤트 + 메시지 브로커)로 가는 게 더 안전함.



🔹 정리: 언제 어떤 방식으로 예외를 처리할까?

🔸 호출자에게 알려야 하는 예외

CompletableFuture/Future로 리턴해서
호출자가 join()/get()에서 CompletionException 처리
→ 그걸 다시 AppException으로 래핑 → @ControllerAdvice에서 HTTP 응답

🔸 호출자에게 알릴 필요 없는 부가 작업 (로그/슬랙/메트릭)

void @Async + AsyncUncaughtExceptionHandler
→ 또는 Kafka 이벤트 Consumer 내부 try-catch에서 로깅 & DLQ

🔸 여러 비동기 호출을 합쳐서 하나의 API 응답을 만들 때

CompletableFuture.allOf() + 각 future에서 exceptionally로 개별 예외 처리
→ 마지막에 CompletionException 잡아서 공통 예외로 변환



🔹 실무에서 언제 Async를 쓰는가?

🔸 오래 걸리는 작업을 사용자 API 응답과 분리할 때

  • 이미지 처리
  • PDF 생성
  • 파일 업로드 후 후처리
  • Slack 알림, 이메일 전송
  • 외부 API 호출 후 로깅

🔸 메시지 큐(Kafka, RabbitMQ) 연결 전 간단 비동기 처리

초기 버전에서 async로 처리하고, 나중에 message queue로 확장하기 좋음.



🔹 상황에 맞는 적절한 사용

  • 단순 비동기는 @Async + ThreadPoolTaskExecutor
  • 여러 비동기 작업 조합, 병렬 처리 → CompletableFuture
  • 대규모 트래픽, 이벤트 처리 필요 → Kafka / RabbitMQ / Event-driven 구조로 확장

profile
꾸준한 공부만이 답이다

0개의 댓글