동기와 비동기 처리는 컴퓨팅에서 작업을 수행하는 방식에 대한 기본적인 접근 방식입니다.
이 두 방식은 작업이 실행되고, 결과가 반환되는 방식이 다릅니다.
동기(Synchronous) 처리
- 모든 작업이 순차적으로 실행 됩니다. 한 작업이 완료되어야만 다음 작업이 시작될 수 있습니다.
- 이는 작업 실행 순서가 명확하고 이해하기 쉽다는 장점이 있지만, 어떤 작업이 많은 시간을 소요할 경우 다음 작업은 그 작업이 완료될 때까지 기다려야 하므로 전체 작업의 실행 시간이 길어질 수 있는 단점이 있습니다.
- 예를 들어, 웹 서버가 클라이언트의 요청을 처리할 때 동기 방식을 사용하면, 요청을 처리하는 동안 다른 요청은 대기 상태에 머물러야 합니다.
비동기(Asynchronous) 처리
- 작업들이 병렬로 실행될 수 있습니다. 즉, 한 작업의 완료를 기다리지 않고, 다음 작업을 시작할 수 있습니다.
- 이는 특히 I/O 작업이 많거나 네트워크 요청이 포함된 경우에 유용하며, 시스템의 전체적인 응답성과 효율성을 높일 수 있습니다.
- 비동기 처리는 결과 처리 방식(콜백, 프로미스, 퓨처 등)으로 인해 프로그램의 로직이 더 복잡해질 수 있습니다.
Spring Boot를 사용한 웹 개발에서는 대부분의 작업이 메서드 호출을 통해 순차적으로 진행되는 동기적인 프로세스를 따릅니다. 이러한 방식은 간단하고 직관적이지만, 특정 작업의 처리 시간이 길어지면 전체 응답 시간도 그만큼 늘어나게 됩니다. 이때 필요한 것이 비동기 처리입니다.
제가 팀 프로젝트를 진행하며 비동기 처리를 적용한 배경을 예로 들어 설명하겠습니다.
사용자의 비밀번호를 재설정하고 해당 비밀번호를 이메일로 전송하는 기능을 구현할 때, 만약 여러명의 사용자가 한 번에 비밀번호 재설정 요청을 한다면 어떻게 될까요?
이메일 전송 과정이 완료될 때까지 사용자는 대기해야 합니다.
이메일 전송은 네트워크 지연, 메일 서버 처리 등 다양한 외부 요인에 의해 처리 시간이 길어질 수 있기 때문에, 이 과정이 동기적으로 처리될 경우 사용자의 대기 시간이 길어져 사용자 경험이 저하됩니다.
이러한 문제를 해결하기 위해 비동기 처리가 필요합니다. 비동기 처리를 이용하면 메일 전송과 같은 시간이 오래 걸리는 작업을 백그라운드에서 처리하고, 그 작업의 완료를 기다리지 않고도 다음 작업을 진행할 수 있습니다.
이렇게 하면 전체 응답 시간을 단축하고, 사용성을 향상시킬 수 있습니다.
스프링부트에서 비동기 처리를 구현하는 방법은 아주 간단합니다.
Java에서의 비동기 처리와는 다르게 개발자들의 번거로움을 덜어주기 위한 어노테이션을 제공합니다.
@Async
어노테이션을 메서드에 추가하면, 스프링은 해당 메서드를 별도의 스레드에서 비동기적으로 실행합니다. 이를 위해 스프링의 비동기 지원을 활성화 하는 @EnableAsync
어노테이션은 Config 클래스에 추가해야 합니다.
@Configuration
@EnableAsync
public class AsyncConfig { }
@Service
public class EmailService {
@Async
public void sendEmail(String email) {
// 이메일 전송 로직
}
}
@Async
어노테이션 덕분에, 이 메서드는 메인 스레드와는 별개의 스레드에서 비동기적으로 실행됩니다. 따라서, 메인 스레드는 이메일 전송 작업의 완료를 기다리지 않고 즉시 다른 작업을 계속할 수 있습니다.
그렇다면 Java에서 비동기 처리는 어떻게 구현할까요?
주로 Future
인터페이스와 CompletableFuture
클래스를 사용합니다. 이를 통해 멀티스레드 프로그래밍을 활용하여 비동기적인 작업을 효율적으로 처리할 수 있습니다.
비동기 처리의 경우, 작업이 완료되면 Future
객체를 통해 결과를 반환합니다.
ExecutorService
는 작업을 수행할 때 원하는 크기만큼의 쓰레드 풀을 생성, 반납하며 관리할 수 있습니다. 작업을 제출하면 그 작업을 수행하고 결과를 Future
객체로 반환합니다.
이때 비동기 메서드가 많이 필요한 경우 메서드를 비동기에 맞게 수정해야 하는 어려움이 있습니다.
ExecutorService executor = Executors.newCachedThreadPool(); // 작업을 비동기적으로 실행
Future<String> futureResult = executor.submit(() -> {
// 비동기 작업
Thread.sleep(1000); // 예시로 1초 대기
return "Hello, World!";
});
// 다른 작업 수행 가능
try {
// Future에서 결과를 가져옴. 결과가 준비될 때까지 대기
String result = futureResult.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
CompletableFuture
는 Future
를 확장한 것으로, 비동기 작업의 결과를 조작하고 연결할 수 있는 방법을 제공합니다.
CompletableFuture는 함수형 프로그래밍 형태의 연결, 에러 처리, 결과 조합 등 다양한 기능을 지원합니다.
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
// 비동기 작업
try {
Thread.sleep(1000); // 예시로 1초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello, World!";
});
// thenApply()을 사용하여 비동기 작업 결과에 대한 후속 처리를 정의
completableFuture.thenApply(result -> result.toUpperCase())
.thenAccept(System.out::println); // 결과 출력
CompletableFuture는 비동기 작업이 완료될 때까지 기다리지 않고, 작업의 결과에 대한 후속 처리를 즉각적으로 정의할 수 있어 매우 유용합니다.
또한, thenCombine
, thenCompose
와 같은 메소드를 통해 여러 비동기 작업의 결과를 조합하거나 연결하는 것도 가능합니다.
비동기 처리란?
현재 진행 중인 작업의 완료 여부와 상관없이 다음 작업을 진행할 수 있게 하는 프로그래밍 방식
매 요청마다 직접 쓰레드를 생성하고 관리하는 것은 어렵습니다.
스프링에서는 개발자를 위해@EnableAsync
와@Async
어노테이션을 제공하여 간편하게 비동기 처리를 구현할 수 있습니다.
필요에 따라 비동기 처리를 적용하여 효율적인 API 개발을 할 수 있습니다.