본문으로 건너뛰기
Advertisement

2.7 비동기 처리 (@Async)

메일 발송·외부 API 호출·알림처럼 응답을 기다리지 않아도 되는 작업은 비동기로 처리하면 요청 스레드를 빨리 놓아줄 수 있습니다. 스프링은 @Async로 메서드를 비동기 실행할 수 있게 해줍니다.

작성 기준: Spring Boot 3.2.x

1. @EnableAsync 및 기본 사용

@SpringBootApplication
@EnableAsync
public class Application { ... }
@Service
public class NotificationService {

@Async
public void sendEmail(String to, String subject, String body) {
// 메일 발송 로직 — 별도 스레드에서 실행
mailSender.send(to, subject, body);
}
}
  • @Async가 붙은 메서드는 다른 스레드에서 실행됩니다.
  • 같은 빈 내부에서 this.sendEmail(...)처럼 호출하면 프록시를 타지 않아 비동기로 동작하지 않습니다. 다른 빈에서 호출해야 합니다.

2. 스레드 풀 설정

기본은 SimpleAsyncTaskExecutor(매번 새 스레드)이므로, 운영에서는 스레드 풀을 지정하는 것이 좋습니다.

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

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

특정 메서드만 다른 풀을 쓰려면 **@Async("customExecutor")**처럼 빈 이름을 지정하고, 해당 Executor 빈을 정의하면 됩니다.

3. 반환값 — CompletableFuture

비동기 결과를 나중에 쓰려면 CompletableFuture<T>를 반환합니다.

@Async
public CompletableFuture<OrderResult> createOrderAsync(OrderRequest request) {
OrderResult result = orderService.create(request);
return CompletableFuture.completedFuture(result);
}

호출부에서는 .get() 또는 .thenAccept(...) 등으로 결과를 사용할 수 있습니다.

4. 예외 처리

@Async 메서드에서 던진 예외는 호출 스레드로 전파되지 않습니다. AsyncUncaughtExceptionHandler를 설정하거나, 반환 타입이 Future/CompletableFuture이면 .get()ExecutionException으로 감싸져 전달됩니다.

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("Async error @{} {}", method.getName(), params, ex);
}

무거운 작업만 @Async로 분리하고, 스레드 풀 크기와 큐 용량을 부하에 맞게 조정하세요. DB/외부 API 호출이 많은 비동기 메서드는 타임아웃·재시도(Retry)와 함께 사용하는 경우가 많습니다.



2.8 스케줄링 (@Scheduled)

정기 배치·통계 집계·캐시 정리처럼 주기적으로 실행해야 하는 작업은 @Scheduled로 등록할 수 있습니다.

작성 기준: Spring Boot 3.2.x

1. @EnableScheduling

@SpringBootApplication
@EnableScheduling
public class Application { ... }

2. 고정 주기 실행

@Component
public class StatsScheduler {

@Scheduled(fixedRate = 60000) // 60초마다 (이전 종료 시점과 무관)
public void collectStats() {
// 통계 수집
}

@Scheduled(fixedDelay = 60000) // 종료 후 60초 뒤 다음 실행
public void cleanup() {
// 정리 작업
}

@Scheduled(initialDelay = 10000, fixedRate = 60000) // 기동 10초 후부터 60초마다
public void sync() {
// 동기화
}
}
  • fixedRate: 시작 시점 기준 간격
  • fixedDelay: 이전 작업 종료 후부터 간격
  • initialDelay: 첫 실행 지연 (ms)

3. Cron 표현식

복잡한 요일·시간대는 cron으로 지정합니다.

@Scheduled(cron = "0 0 2 * * ?")  // 매일 새벽 2시
public void dailyReport() { ... }

@Scheduled(cron = "0 */10 * * * ?") // 10분마다
public void everyTenMinutes() { ... }

형식: 초 분 시 일 월 요일 [년]

  • 요일: 0–7 (0, 7 = 일요일), MON–SUN 가능
  • Spring: 6자리 (년 생략)

4. 프로파일/조건으로 스케줄 끄기

특정 환경에서만 스케줄을 돌리려면 @Profile 또는 @ConditionalOnProperty를 사용합니다.

@Component
@Profile("!test")
@ConditionalOnProperty(name = "app.scheduler.enabled", havingValue = "true", matchIfMissing = true)
public class StatsScheduler { ... }

5. 단일 인스턴스 실행 (분산 환경)

서버가 여러 대일 때 같은 작업이 중복 실행되지 않게 하려면 ShedLock 또는 DB/Redis 기반 락을 사용합니다. 스프링 기본 @Scheduled만으로는 인스턴스 간 조율이 되지 않습니다.


긴 작업은 스레드 풀 크기를 TaskScheduler로 제한하고, 필요하면 @Async와 조합해 별도 스레드에서 실행하도록 구성하는 것이 좋습니다.



2.12 재시도와 복원력 (Retry / Resilience4j)

외부 API·DB·메시지 브로커 호출은 일시적 장애로 실패할 수 있습니다. 재시도(Retry)서킷 브레이커로 실패를 흡수하고, 장애 전파를 줄일 수 있습니다.

작성 기준: Spring Boot 3.2.x, spring-retry 또는 Resilience4j

1. Spring Retry (@Retryable)

implementation 'org.springframework.retry:spring-retry'
@SpringBootApplication
@EnableRetry
public class Application { ... }
@Service
public class PaymentClient {

@Retryable(
retryFor = { RestClientException.class },
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public PaymentResult callExternalApi(PaymentRequest req) {
return restTemplate.postForObject(...);
}

@Recover // 모든 재시도 실패 후 호출
public PaymentResult recover(RestClientException e, PaymentRequest req) {
log.error("결제 API 호출 실패, 폴백", e);
return PaymentResult.failed("일시적 오류");
}
}
  • retryFor: 이 예외일 때만 재시도
  • maxAttempts: 최대 시도 횟수
  • backoff: 대기 시간 (delay ms, multiplier로 증가)
  • @Recover: 시그니처가 (Exception, 메서드 인자...)와 일치해야 함

2. Resilience4j — 서킷 브레이커

연속 실패 시 일정 시간 호출을 차단해 리소스를 보호하려면 Resilience4j를 사용합니다.

implementation 'io.github.resilience4j:resilience4j-spring-boot3'
resilience4j:
circuitbreaker:
instances:
paymentApi:
registerHealthIndicator: true
slidingWindowSize: 10
minimumNumberOfCalls: 5
failureRateThreshold: 50
waitDurationInOpenState: 10s
@Service
public class PaymentService {

private final CircuitBreaker circuitBreaker;
private final PaymentClient client;

public PaymentService(CircuitBreakerRegistry registry, PaymentClient client) {
this.circuitBreaker = registry.circuitBreaker("paymentApi");
this.client = client;
}

public PaymentResult pay(PaymentRequest req) {
return circuitBreaker.executeSupplier(() -> client.callExternalApi(req));
}
}
  • failureRateThreshold: 실패 비율이 이 값을 넘으면 서킷이 OPEN (호출 차단)
  • waitDurationInOpenState: OPEN 상태 유지 시간 후 HALF_OPEN으로 전환해 재시도

3. Retry + CircuitBreaker 조합

Resilience4j는 RetryCircuitBreaker를 같이 등록할 수 있습니다.
먼저 Retry로 몇 번 재시도하고, 그래도 실패율이 높으면 CircuitBreaker가 열리도록 구성하는 패턴이 많습니다.


재시도할 예외를 retryFor로 한정하고, 비즈니스 예외(4xx 등)는 재시도하지 않도록 구분하는 것이 좋습니다. 외부 의존성 타임아웃은 RestTemplate/WebClient 설정과 함께 조정하세요.

Advertisement