비동기(Asynchronous)란?
작업이 시작된 후 그 결과를 기다리지 않고 다른 작업을 계속 진행할 수 있는 처리 방식을 의미한다.
즉, 작업이 실행되는 동안 해당 작업이 완료될 때 까지 기다리지 않고, 다른 작업을 수행한다.
예를 들어 이메일 보내기 서비스를 개발한다고 하면,
사용자가 서버에 요청했을 때 이메일 보내는 기능은 비동기로 처리하고 이메일 보내는 것과 무관하게 200을 응답한다. 따라서 사용자는 이메일이 보내지는 것을 기다릴 필요 없어진다.
- 작업: 이메일 보내기, 200 응답 보내기
- 시간이 오래 걸리는 작업, 비동기 시킬 직업: 이메일 보내기
- 비동기와 별개로 바로 수행할 작업: 200 응답 보내기
왜 사용할까?
- 응답성 향상: 비동기를 사용하면 사용자가 상호작용 하는 동안에도 백그라운드에서 작업을 계속 진행할 수 있어 응답성이 향상된다.
- 사용자 경험 향상: 사용자는 비동기 작업을 기다릴 필요 없이 바로 응답을 받을 수 있어 경험이 향상된다.
@Async란?
@Async 란 스프링에서 비동기 메서드 실행을 지원하기 위한 기능으로, 내부적으로 프록시 객체, 스레드 풀, AOP 기반으로 동작한다.
- 프록시 객체: 다른 객체에 대한 대리자 역할을 하는 객체
- 스레드 풀: 미리 생성된 스레드의 집합. 스레드 풀은 작업을 큐에 저장하고 스레드가 사용 가능할 때 큐에서 작업을 가져와 스레드가 적업을 하게 한다.
- AOP: 코드의 중복을 줄이고 공통된 기능을 효율적으로 관리
스프링은 @Async 가 붙은 메서드에 대해 프록시 객체를 생성하고 본 메서드 대신 프록시를 사용한다.
프록시 객체는 해당 메서드가 비동기로 실행되도록 스레드 풀에 작업을 제출해 별도의 스레드에서 작업이 실행되도록 한다.
동작 원리
- 스프링에 메서드에 @Async 를 발견하면, AOP 기반으로 프록시 객체를 생성해 해당 메서드를 비동기 방식으로 실행하도록 처리한다.
- 메서드가 호출되면 즉시 리턴되고, 로직은 백그라운드에서 실행된다.
- 반환 타입이 Future, CompletableFuture인 경우 후속 작업도 가능하다.
- 프록시 객체는 해당 메소드가 비동기적으로 실행되도록 스레드 풀에 작업을 제출하고, 원래 호출한 스레드는 즉시 반환한다. 즉, 호출자는 메서드가 끝날 때까지 기다리지 않고 바로 다음 작업을 수행할 수 있다.
비동기 사용법
1. Configuration 파일을 구성합니다.
@Configuration
@EnableAsync
public class AsyncConfig {
}
- @EnableAsync : 스프링 애플리케이션에서 비동기 처리를 활성화하기 위한 애너테이션. @Async 와 함께 사용되어 스프링이 비동기적으로 실행할 수 있는 메서드를 감지하여 백그라운드 스레드에서 실행할 수 있도록 설정해준다.
2. 비동기로 실행할 메서드에 대해 @Async 를 명시합니다.
@Async
public void sendEmail(
String toEmail,
String title,
String text
) {
SimpleMailMessage emailForm = createEmailForm(toEmail, title, text);
// 이메일 전송
try {
emailSender.send(emailForm);
} catch (RuntimeException e) {
throw ApiException.from(EMAIL_BAD_GATEWAY);
}
}
주의
- @Async 가 붙은 메서드는 같은 클래스에서 호출하면 안된다. 클래스 외부에서 해당 메서드를 호출해야만 동작한다. 이는 스프링 AOP의 동작 원리 때문이다. 같은 클래스 내의 메소드 호출은 프록시가 아닌 직접적인 호출로 처리되므로 비동기 처리가 되지 않는다.
- 비동기 작업은 다른 스레드에서 실행되므로 트랜잭션 범위를 벗어날 수 있다. 즉, 비동기 메소드가 트랜잭션에서 실행되더라도 트랜잭션이 종료된 후에 해당 메소드가 실행될 수 있습니다.
- 스프링의 트랜잭션은 같은 스레드 내에서 실행되는 코드에만 영향을 미친다.
- 비동기 작업 내에서 트랜잭션이 필요한 경우, @Async 메소드 자체에 @Transactional을 추가하여 별도의 트랜잭션을 명시적으로 시작해야 한다.
- 다만 이 경우 메인 쓰레드의 트랜잭션과 @Async 내의 트랜잭션은 별개로 동작하고, 두 트랜잭션은 서로 영향을 미치지 않는다.
3. (+추가) 스레드 풀 관리
아래의 스레드 풀 설정은 선택이다. 이 설정을 안하면 비동기 스레드 설정이 디폴트 값으로 들어간다.
디폴트로 SimpleAsyncTaskExecutor를 사용하며, 이 기본 설정은 스레드 풀을 사용하지 않고 매번 새로운 스레드를 생성하는 방식이다.
다만 매번 새로 스레드를 사용하는 것은 비효율적일 수 있으므로 아래와 같이 스레드 설정을 해줄 수 있다.
- 참고로 이 스레드 풀 설정은 전역 스레드 설정이 아니라 오직 비동기에 사용될 스레드 설정이다.
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 기본 스레드 수
executor.setMaxPoolSize(20); // 최대 스레드 수
executor.setQueueCapacity(100); // 요청 큐 크기
executor.setThreadNamePrefix("AsyncThread-");
executor.initialize();
return executor;
}
}
비동기 메서드의 반환 타입
@Async 메소드는 기본적으로 백그라운드 스레드에서 실행되기 때문에, 호출한 메인 스레드는 결과를 즉시 반환받지 못하고 결과를 추후 비동기적으로 처리할 수 있다.
따라서 비동기적으로 처리할 수 있는 여러 반환 타입을 제공하며, 반환 타입에 따라 결과를 기다리거나 추가적인 비동기 작업을 체인으로 연결할 수 있다.
1. void
void: 반환값이 없는 경우, 메소드가 호출된 후 결과를 신경 쓰지 않겠다는 뜻
즉 void로 비동기 메서드를 리턴하면 호출한 쪽에서는 아무것도 하지 않는다.
@Async
public void asyncMethod() {
System.out.println("비동기 작업 실행 중...");
}
- 비동기 작업의 결과를 확인할 필요가 없을 때 사용한다.
2. Future
Future: 비동기 작업의 결과를 나중에 받을 수 있도록 Future 객체를 반환할 수 있다. 작업이 완료되면 Future.get()을 호출해 결과를 얻는다.
@Async
public Future<String> asyncMethod() {
try {
Thread.sleep(2000); // 비동기 작업 시뮬레이션
return new AsyncResult<>("작업 완료");
} catch (InterruptedException e) {
return new AsyncResult<>("오류 발생");
}
- Future 객체를 반환받고, 나중에 get() 메서드를 통해 결과를 확인할 수 있다.
- 이 때 작업이 완료될 때까지 get() 메서드는 블로킹된다.
Future<String> futureResult = myService.asyncMethod();
String result = futureResult.get(); // 결과가 완료될 때까지 대기 (Blocking)
System.out.println(result); // 작업 완료 시 결과 출력
- get() 이후의 코드들은 블로킹되어 응답을 받을 때까지 실행되지 않는다.
- 이는 비동기의 이점이 사라지게 되므로(원래라면 이후의 코드가 별개로 실행된다.) 사용하지 않는 것이 좋을 거 같다.
3. ComletableFuture
- CompletableFuture: CompletableFuture는 비동기 작업에 대해 더 다양한 연산(예: 후속 작업)을 할 수 있는 기능을 제공한다. thenApply(), thenCompose()와 같은 메소드를 통해 비동기 작업을 연속적으로 실행할 수 있다.
@Async
public CompletableFuture<String> asyncMethod() {
try {
Thread.sleep(2000); // 비동기 작업 시뮬레이션
return CompletableFuture.completedFuture("작업 완료");
} catch (InterruptedException e) {
return CompletableFuture.completedFuture("오류 발생");
}
}
- CompletableFuture 는 비동기 작업이 완료된 후 후속 작업을 thenApply() , thenRun() , thenAccept() 등의 메서드를 통해 정의할 수 있다.
CompletableFuture<String> completableFuture = myService.asyncMethod();
completableFuture
.thenAccept(result -> System.out.println("비동기 작업 결과: " + result))
.exceptionally(ex -> {
System.out.println("예외 발생: " + ex.getMessage());
return null;
});
System.out.println("안녕");
- CompletableFuture 와 무관한 코드들은 논 블로킹으로 실행됩니다. 즉 CompletableFuture 응답보다 안녕이 먼저 출력될 수 있다.
thenApply()
thenApply() : 비동기 작업의 결과를 받아서 새로운 값을 반환하는 작업을 수행할 때 사용
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> futureResult = future.thenApply(result -> result + " World!"); // 결과를 변환
thenRun()
thenRun() :비동기 작업의 결과를 사용하지 않고, 그 작업이 완료된 후 새로운 작업을 실행하는 경우에 사용
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");
future.thenRun(() -> System.out.println("비동기 작업 완료 후 실행"));
thenAccept()
thenAccept() :비동기 작업의 결과를 소비(처리)하는 작업을 수행할 때 사용
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");
future.thenAccept(result -> System.out.println("결과: " + result)); // 결과 소비 (출력)
비동기 작업의 예외 처리
비동기 작업에서 예외는 CompletableFuture 를 통해 후속 처리 할 수 있다.
@Async
public CompletableFuture<String> asyncMethodWithError() {
try {
// 작업 도중 예외 발생
throw new RuntimeException("Error occurred");
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
exceptionally()
future
.exceptionally(ex -> {
System.out.println("예외 발생: " + ex.getMessage());
return "대체 값";
})
.thenAccept(result -> System.out.println("결과: " + result));
- exceptionally는 예외가 발생했을 때만 실행되며, 예외를 처리하고 대체 값을 반환한다.
- 그럼 thenAccept가 실행된다.
handle()
future
.handle((result, ex) -> {
if (ex != null) {
System.out.println("예외 발생: " + ex.getMessage());
return "예외 발생 시 반환할 값";
}
return result;
})
.thenAccept(result -> System.out.println("결과: " + result));
- handle은 정상 결과, 예외 모두를 처리할 수 있다. 예외가 발생하든 정상적으로 완료되든 후속작업을 처리할 수 있다.
whenComplete()
future
.whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("예외 발생: " + ex.getMessage());
} else {
System.out.println("결과: " + result);
}
});
- handle과 유사하게 정상 결과, 예외 모두를 처리하지만, 새로운 값을 반환하지 않고 단순히 후속 작업만 처리한다.
'Spring' 카테고리의 다른 글
Spring에서의 예외, 에러 처리 (0) | 2024.11.02 |
---|---|
Spring에서의 프로세스와 스레드 (0) | 2024.10.21 |
Spring에서의 동기, 비동기 (1) | 2024.09.26 |
java PageImpl에서 totalElements가 바뀌는 오류 해결 (0) | 2024.08.13 |
[개발 일상] Spring 공통 응답 만들기 (0) | 2024.08.13 |