- 여러 스레드를 동시에 실행하여 작업을 ‘병렬로 처리’하는 기술을 의미합니다. 이는 CPU의 활용도를 높이고, 응답 시간을 줄이며, 서버의 처리 능력을 향상합니다.
- 이러한 비동기 작업은 별도의 스레드에서 실행이 되며, 메인 스레드가 블로킹되지 않도록 합니다. 즉, 메인 스레드가 특정 작업을 기다리느라 멈추지 않고, 다른 작업을 계속해서 수행할 수 있습니다.
[ 더 알아보기 ] 💡 병렬 처리
- 여러 작업을 동시에 수행하는 기술을 의미합니다. 이는 여러 개의 프로세서나 스레드를 사용하여 작업을 병렬로 실행함으로써, 작업 처리 시간을 줄이고 시스템 성능을 향상하는 방법입니다. - 주로 대규모 연산 데이터 처리, 시뮬레이션 등에 사용되며, 이를 통해 더욱 빠르고 효율적인 작업처리가 가능합니다.
💡 그럼 일반적으로 사용하는 API 서버는 동기적 처리라고 알고 있는데, 이는 멀티스레드를 사용하지 않는 것일까?
- 일반적인 동기 처리에서도 멀티스레드를 사용할 수 있습니다. 동기 처리는 작업이 완료될 때까지 호출한 메서드가 반환되지 않는 것을 의미하며, 멀티스레드는 여러 스레드를 동시에 실행하여 병렬로 작업을 처리하는 기술입니다. - 따라서 동기 처리에서도 멀티스레드를 활용하여 작업을 병렬로 실행할 수 있습니다. 단, 동기 처리에서는 작업 간의 순서가 보장되며, 비동기 처리와는 다르게 호출한 메서드가 완료될 때까지 기다리는 특성이 있습니다.
- 여러 개의 스레드들을 ‘미리 생성’해 두고 작업 큐에서 작업을 가져와서 실행하는 방식으로, 스레드 생성과 소멸에 따른 오버헤드를 줄이고, 시스템 자원을 효율적으로 활용할 수 있게 합니다.
- 이를 사용하면 스레드 생성 비용을 절감하고, 응답 시간을 단축시키며, 시스템의 안정성을 높일 수 있습니다.
💡 스레드 풀 생성 과정
- 애플리케이션이 시작될 때, 사전에 미리 지정한 스레드의 개수에 따라서 스레드 풀 내에 스레드가 생성이 됩니다.
💡 스레드 풀 처리 과정 - 생성된 스레드를 기반으로 이를 이용한 처리과정을 이해합니다
1. 애플리케이션(Application)
- 비동기 처리의 하나의 새로운 작업(New Task)을 발생시켰고, 이를 작업 큐로 제출(Submit)을 하게 됩니다.
2. 작업 큐(Task Queue)
- 전달받은 작업(Task)은 큐에서 스레드의 사용이 가능할 때까지 보관을 합니다. - 또한 큐에서는 FIFO 형태로 먼저 들어온 데이터가 먼저 처리하는 구조를 가지며 순차적으로 데이터가 쌓이고 들어온 순서대로 처리가 수행됩니다.
3. 스레드(Thread)
- 스레드 풀에 있는 스레드 중 하나가 사용이 가능해지면, 작업 큐에서 작업을 가져와서 실행을 하게 됩니다. - 작업이 완료되면, 스레드는 유휴 상태가 되며, 작업이 발생하면 다시 큐에서 다음 작업을 가져와서 처리를 합니다.
💡 Java 내에서 스레드 풀 생성 및 처리 과정 1. 스레드 풀 생성 : ExecutorService
- 스레드 풀을 생성할 때, 미리 정해진 수의 스레드가 생성됩니다. Java에서는 ExecutorService 인터페이스를 통해 스레드 풀을 생성할 수 있습니다
2. 작업 제출 : submit()
- 스레드 풀에 작업을 제출하면, 작업은 작업 큐에 추가됩니다. 큐에 추가된 작업은 스레드 풀 내의 스레드에 의해 실행됩니다.
3. 작업 실행
- 스레드 풀 내의 스레드가 작업 큐에서 작업을 가져와 실행합니다. 스레드가 유휴 상태일 때, 다음 작업을 가져와 실행합니다.
4. 작업 완료 - 각 스레드는 작업을 완료하면 다시 유휴 상태로 돌아가며, 다음 작업을 대기합니다.
5. 스레드 풀 종료 : shutdown()
- 더 이상 작업을 제출하지 않을 때, 스레드 풀을 종료해야 합니다. 이를 위해 shutdown() 또는 shutdownNow() 메서드를 호출합니다. - shutdown()은 이미 제출된 작업을 완료한 후 스레드 풀을 종료하고, shutdownNow()는 실행 중인 작업을 중단하고 즉시 종료합니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
publicclassThreadPoolExample {
publicstaticvoidmain(String[] args) {
// 스레드 풀 생성ExecutorServiceexecutorService= Executors.newFixedThreadPool(5);
// 작업 제출for (inti=0; i < 10; i++) {
executorService.submit(() -> {
System.out.println("Thread Name: " + Thread.currentThread().getName());
});
}
// 스레드 풀 종료
executorService.shutdown();
}
}
💡 스레드 풀 생성 및 처리 과정 결과
[ 더 알아보기 ] 💡 스레드 풀을 사용하지 않으면 어떻게 될까?
- 스레드 풀을 사용하지 않으면, 각 작업이 요청될 때마다 새로운 스레드가 생성되고 작업이 완료되면 해당 스레드가 소멸됩니다. - 이는 스레드 생성과 소멸에 따른 오버헤드가 발생하여 시스템 자원의 비효율적인 사용을 초래할 수 있습니다. - 또한, 스레드 수가 무제한으로 증가할 수 있어 시스템 성능이 저하되거나 심지어 시스템이 다운될 위험이 있습니다.
💡 스레드 풀이랑 커넥션 풀이랑 비슷한 개념인 거 같은데?
- 스레드 풀과 커넥션 풀은 개념적으로 유사합니다. 두 경우 모두 자원을 미리 할당하여 필요할 때 빠르게 사용할 수 있도록 하여, 자원 생성 및 소멸에 따른 오버헤드를 줄이고 시스템 성능을 향상하는 역할을 합니다. - 스레드 풀은 스레드를 미리 생성해 두고 작업을 처리하며, 커넥션 풀은 데이터베이스 연결을 미리 생성해 두고 필요할 때 재사용합니다.
💡 [참고] Connection Pool을 이용한 DBCP에 대한 이해가 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
- 주로 @Configuration을 선언한 클래스에서 사용됩니다. 해당 어노테이션을 선언한 클래스 내에서는 메서드 단위로 비동기 처리 활성화 설정을 하거나 @Bean 어노테이션과 함께 Executor의 스레드 풀 설정이나 비동기 작업에 대한 설정을 정의하는 데 사용이 됩니다.
1.1. @EnableAsync 어노테이션 속성
💡 @EnableAsync 어노테이션 속성
속성 명
리턴 타입
default
설명
annotation
Class<? extends Annotation>
클래스 또는 메소드 수준에서 감지할 'async' 어노테이션 타입을 나타냅니다.
mode
AdviceMode
AdviceMode.PROXY
비동기 어드바이스가 적용될 방식을 나타냅니다. AdviceMode.PROXY AdviceMode.ASPECTJ을 제공합니다.
order
int
-
AsyncAnnotationBeanPostProcessor가 적용될 순서를 나타냅니다.
proxyTargetClass
boolean
false
표준 Java 인터페이스 기반 프록시 대신 서브클래스 기반(CGLIB) 프록시가 생성될지 여부를 나타냅니다.
- 메서드를 ‘비동기 메서드’로 실행하도록 지정하는 어노테이션입니다. 즉, 메서드 호출은 즉시 반환되고, 실제 작업은 별도의 스레드에서 비동기적으로 수행이 됩니다.
- 해당 어노테이션이 지정된 메서드는 반드시 접근 제한자로 public을 선언해야 합니다. 이는 메서드가 public 이어야 프록시 될 수 있기 때문입니다.
[ 더 알아보기] 💡 프록시 될 수 있다는 말은 뭘까?
- 메서드가 호출될 때, 실제 메서드 호출을 가로채서 별도의 로직을 추가하거나 대체할 수 있는 프록시 객체가 사용된다는 의미입니다. - Spring에서는 주로 AOP(Aspect-Oriented Programming)를 통해 이런 프록시 패턴을 사용하여 비동기 처리, 트랜잭션 관리, 로깅 등의 기능을 구현합니다.
- @Async 어노테이션을 사용하는 메서드 내에서 비동기적으로 실행되며 호출자는 즉시 반환이 됩니다. 이때 메서드는 처리 결과를 반환하지 않습니다. - 반환 유형이 존재하지 않는 경우는 단순 작업이나 I/O 작업에서 주로 사용이 됩니다. - 예를 들어, 로그 기록이나 알림 전송과 같은 작업들이 이에 해당합니다.
💡 반환 유형이 존재하지 않는 경우 사용예시
- @Async 어노테이션을 통해서 메서드 호출이 비동기적으로 처리되며, 메서드의 리턴타입이 void로 반환되지 않으며 비동기 처리가 되는 메서드를 의미합니다. - 해당 메서드 내에서는 별도의 리턴값을 받지 않고 처리가 됩니다
/**
* [Async] 반환 유형이 존재하지 않는 경우 : void
*/@Async@OverridepublicvoidasyncVoidType() {
System.out.println("Execute method asynchronously. :: " + Thread.currentThread().getName());
}
- Java의 비동기 프로그래밍에서 사용되는 인터페이스로 비동기 작업의 결과를 나타내는 클래스입니다. 이는 작업이 완료되었는지 여부를 확인하고, 완료된 작업의 결과를 가져오거나, 작업이 완료될 때까지 기다리는 등의 기능을 제공합니다.
- Future 객체의 get 메서드를 호출하면 비동기 작업이 완료될 때까지 블로킹됩니다. 따라서 get 메서드를 호출할 때는 주의가 필요합니다. - 주로 결과를 필요로 하는 복잡한 작업이나 시간이 오래 걸리는 작업에 사용됩니다. - 예를 들어, 데이터베이스 쿼리나 파일 처리 작업 등이 이에 해당합니다.
- Future의 확장으로 비동기 작업의 완료를 기다리는 동안 '콜백'을 등록하여 작업이 완료될 때 특정 동작을 실행할 수 있는 기능을 제공합니다.
- 해당 객체를 통해 비동기 작업이 완료되었을 때, 특정 작업을 수행할 수 있도록 콜백을 설정할 수 있습니다. 이는 비동기 작업의 완료를 기다리지 않고도 후속 작업을 설정할 수 있어서 효율적인 비동기 프로그래밍을 가능하게 합니다. - Spring Framework 6.x 이상 버전에서 해당 클래스는 Deprecated 되었습니다. - 주로 결과를 필요로 하는 복잡한 작업이나 시간이 오래 걸리는 작업에 사용됩니다.
[ 더 알아보기 ]
💡 콜백이란 무엇일까?
- 콜백은 특정 작업이 완료된 후 자동으로 호출되는 함수 또는 메서드를 의미합니다. 비동기 작업은 메인 스레드와 별도로 실행되기 때문에, 작업이 완료되었을 때 어떤 작업을 수행해야 하는지 지정할 필요가 있습니다. 이때 콜백 메서드를 사용하여 작업 완료 후의 처리될 내용에 대해서 정의합니다. - 주로 비동기 프로그래밍에서 사용되며, 비동기 작업이 완료된 후 후속 작업을 수행하는 데 유용합니다.
3.1. ListenableFuture 메서드
💡 ListenableFuture 메서드
- 해당 객체는 interface java.util.concurrent.Future로부터의 데이터를 상속받아서 모두 이용이 가능하면서 ListenableFuture 객체만의 메서드 이용이 가능합니다. - 해당 메서드는 모두 Spring 6.x 버전에서는 Deprecated 되었고 CompletableFuture를 사용하기를 권장하고 있습니다.
메서드
리턴 타입
설명
addCallback(ListenableFutureCallback<? super T> callback)
void
비동기 작업이 완료되었을 때 실행될 콜백을 등록합니다.
addCallback(SuccessCallback<? super T> successCallback, FailureCallback failureCallback)
💡 ListenableFuture 사용 예시 - asyncListenableFuture() 메서드를 구성하였습니다. 해당 메서드는 @Async를 통해서 비동기 처리를 수행하는 메서드로 선 언하였고, ListenableFuture <String> 객체의 리턴타입을 가진 형태입니다.
1. 현재 스레드의 이름을 출력합니다. - 스레드가 생성되고 반환되는 과정을 확인하기 위해 로그를 작성하였습니다.
2. 스레드를 5초간 일시 중지 시킵니다. - 비동기 메서드의 실행 중에 일정 시간을 기다리게 하기 위해서입니다. 5초 동안 스레드를 일시 중지하고 그 후 비동기 작업의 결과를 반환합니다. 3. 비동기 작업의 결과를 반환합니다. - ListenableFuture <String> 리턴타입에 맞는 형태로 객체를 생성하였습니다. 4. 콜백을 등록합니다. - addCallback() 메서드를 통해서 성공 시, 실패 시에 대한 콜백함수를 받습니다. 각각 성공에 따르는 로그와 실패에 따르는 로그를 작성하였습니다.
import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.util.concurrent.ListenableFuture;
// @Async 어노테이션으로 비동기 메서드를 정의합니다.@Async@Overridepublic ListenableFuture<String> asyncListenableFuture() {
// 1. 현재 스레드의 이름을 출력합니다.
System.out.println("Execute method asynchronously - " + Thread.currentThread().getName());
try {
// 2. 스레드를 5초간 일시 중지 시킵니다.
Thread.sleep(5000);
// 3. 비동기 작업의 결과를 반환합니다.
ListenableFuture<String> future = newAsyncResult<>("hello world !!!!");
// 4. 콜백을 등록합니다.
future.addCallback(newListenableFutureCallback<String>() {
@OverridepublicvoidonSuccess(String result) {
// 비동기 작업이 성공적으로 완료되었을 때 실행되는 콜백
System.out.println("Success with result: " + result);
}
@OverridepublicvoidonFailure(Throwable t) {
// 비동기 작업이 실패했을 때 실행되는 콜백
System.out.println("Failure: " + t.getMessage());
}
});
return future;
} catch (InterruptedException e) {
System.out.println("error :: " + e.getMessage());
}
returnnull;
}
💡 [참고] 'org.springframework.util.concurrent.ListenableFuture' is deprecated since version 6.0
- Spring 버전 6.0부터 이 클래스는 더 이상 사용되지 않도록(deprecated) 표시되었습니다. - 이는 해당 클래스가 더 이상 권장되지 않으며, 향후 버전에서는 제거될 수 있음을 의미합니다. 대신 CompletableFuture와 같은 대체 클래스를 사용하는 것이 권장됩니다. CompletableFuture는 Java 8에서 도입되었으며, 비동기 프로그래밍을 더 쉽고 유연하게 할 수 있는 다양한 기능을 제공합니다.
- Java 8에서 도입된 Future의 구현체이자 확장 기능을 제공하는 클래스입니다. 이전 Future 보다 비동기 작업을 더 쉽게 작성하고 관리할 수 있도록 도와줍니다.
- 주요 기능으로는 작업을 체인 방식으로 연결할 수 있는 thenApply, thenAccept, thenCompose 등의 메서드와, 작업 완료 후 특정 동작을 실행할 수 있는 whenComplete, handle 등의 메서드가 있습니다. - 또한, CompletableFuture는 명시적으로 완료 상태로 설정할 수 있어, 외부에서 작업의 성공 또는 실패를 제어할 수 있습니다. - 주로 비동기 작업의 결과를 조합하거나, 체이닝 하여 연속적인 비동기 작업을 수행할 때 사용됩니다. 예를 들어, 여러 비동기 작업을 순차적으로 실행하거나 결과를 조합하는 데 유용합니다.
💡 CompletableFuture 사용 예시 -2 - 해당 예시에서는 콜백으로 성공/실패에 대한 후처리가 포함된 예시입니다. 1. CompletableFuture 객체를 생성하고, 비동기 작업을 수행하여 결과값을 반환받습니다. 2. 비동기 작업이 완료되었을 때 수행할 동작을 정의합니다. 3. 비동기 작업의 성공과 실패에 따라 다르게 처리합니다.
4. 메인 스레드가 종료되지 않도록 2초간 대기합니다.
5. 결과적으로 CompletableFuture 객체를 반환합니다.
@Overridepublic CompletableFuture<String> asyncCompletableFuture2() {
// 1. CompletableFuture 객체를 생성하고, 비동기 작업을 수행하여 결과값을 반환받습니다.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");
// 2. 비동기 작업이 완료되었을 때 수행할 동작을 정의합니다.
future.whenComplete((result, exception) -> {
if (exception == null) {
// 3. 비동기 작업이 성공적으로 완료되었을 때 실행되는 블록
System.out.println("Completed successfully with result: " + result);
} else {
// 4. 비동기 작업이 실패했을 때 실행되는 블록
System.out.println("Completed with error: " + exception.getMessage());
}
});
// 5. 메인 스레드가 종료되지 않도록 2초간 대기합니다.try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("error :: " + e.getMessage());
}
// 6. CompletableFuture 객체를 반환합니다.return future;
}
- 비동기 작업이 완료될 때까지 메인 스레드가 살아있도록 하기 위해서입니다. 만약 메인 스레드가 먼저 종료되면, 비동기 작업이 완료되기 전에 프로그램이 종료될 수 있습니다. - 따라서 비동기 작업이 완료되는 것을 보장하기 위해 메인 스레드를 일시적으로 대기 상태로 만들어 주는 것입니다.
💡 그러면 메인 스레드는 항상 대기하도록 sleep()을 해주어야 하는가?
- 그렇지 않습니다. 일반적으로는 비동기 작업이 완료될 때까지 메인 스레드가 대기할 필요는 없습니다. - 비동기 작업이 완료되면, 해당 작업의 콜백 또는 후속 작업이 자동으로 실행됩니다. - 다만, 예제 코드에서처럼 프로그램이 종료되지 않도록 하기 위해 일시적으로 대기 상태로 만들어 주는 경우가 있을 수 있습니다. 하지만 실제 애플리케이션에서는 비동기 작업을 처리하는 다른 방법을 사용할 수 있습니다.
💡Thread.currentThread(). getName()로 조회되는 것은 메인 스레드인가?
- Thread.currentThread().getName()로 조회되는 것은 메인 스레드가 아닙니다. - @Async 어노테이션을 사용하여 비동기 메서드를 정의하면, 해당 메서드는 별도의 스레드에서 실행됩니다. - 따라서, Thread.currentThread().getName()를 호출하면 현재 실행 중인 비동기 스레드의 이름이 반환됩니다.
💡 [참고] 해당 글에서 테스트한 코드는 아래의 Repository 내에서 확인이 가능합니다