Spring 비동기 처리

이종찬·2023년 2월 15일
3

📖 Spring에서 비동기 처리?

Spring Boot에서 비동기 처리는 멀티스레딩 환경에서 비동기적으로 실행되는 작업을 처리하는 것으로, 동기적인 방식과 비교해 처리 속도와 성능을 개선할 수 있습니다.

비동기 처리 작업이란 멀티스레드를 사용하여 작업을 분리하고, 작업이 끝날 때까지 대기하지 않고 다른 작업을 처리할 수 있습니다.

스프링부트에서는 @Async 어노테이션과 CompletableFuture클래스를 사용하여 비동기 처리를 구현할 수 있습니다. @Async는 메서드를 비동기로 실행하도록 설정하고, CompletableFuture 클래스는 비동기 작업의 결과를 처리하는데 사용됩니다.

📖 @Async

@Async어노테이션은 Spring이 제공하는 기능으로, 해당 어노테이션이 붙은 메서드를 비동기 처리로 실행할 수 있도록 해줍니다. 이를 사용하면 메서드가 실행되는 동안 다른 작업을 수행할 수 있으며, 작업의 완료 여부를 확인할 수 있습니다.

📖 CompletableFuture

CompletableFuture는 비동기 처리를 위한 인터페이스입니다. 비동기 작업이 완료된 이후 결과값을 처리할 수 있는 다양한 메서드를 제공합니다. CompletableFuture를 사용하면 비동기 처리 결과를 다루는 것이 보다 간편해지며 메서드 체인을 통해 비동기 작업의 연속을 구현할 수 있습니다.

🤔 사용해야 하는 이유는?

1. 높은 응답성

동기적인 방식으로 작업을 처리할 때, 작업이 끝날 때까지 다른 요청을 처리할 수 없습니다. 하지만 비동기 처리는 작업이 완료되기를 기다리지 않기 때문에 요청에 대한 응답 시간을 줄일 수 있습니다. 예를 들어 외부 서비스와 통신할 때 I/O 작업이 많은 경우 작업이 끝날 때 까지 기다리는 대신 다른 요청을 처리하며 시간을 절약할 수 있습니다.

2. 자원 효율성

동기적인 방식으로 작업을 처리할 때는 스레드를 많이 생성해야 하기 때문에 시스템 자원을 많이 사용합니다.

비동기처리는 작업이 끝날 때까지 스레드를 차지하지 않기 때문에 자원을 효율적으로 사용할 수 있습니다. 또한 스레드가 많아짐에 따라 처리량을 늘리는 것이 가능해 시스템이 확장될 때도 확장성을 유지할 수 있습니다.

즉, 비동기 처리를 이용하여 자원을 효율적으로 분배하여 응답시간을 단축하는 것이 주 목적입니다.


😮 사용되는 사례

1. 외부 API 호출

외부 API를 호출할때 해당 API의 응답을 기다리는 동안 서버의 자원이 블로킹될 수 있습니다. 이를 방지하기 위해 비동기 처리를 사용하여 API 호출 결과를 기다리지 않고, 다른 작업을 수행할 수 있습니다.

2. 데이터베이스 작업

데이터 베이스 작업은 I/O 작업으로 대기 시간이 길어질 수 있습니다. 비동기 처리르 사용하여 데이터베이스 작업을 백그라운드 스레드에서 실행하고, 결과값을 반환받을 때까지 다른 작업을 수행할 수 있습니다.

3. WebSocket통신

실시간 양방향 통신을 지원하기 때문에 클라이언트와 서버 간에 계속해서 데이터를 주고받아야 합니다. 비동기 처리를 사용하면 서버 처리량을 높이고, 응답시간을 단축할 수 있습니다.

그 밖에도 파일 업로드 및 다운로드, 응답까지의 시간이 걸리는 비즈니스로직 등에 경우에 사용됩니다. 주로 많은 양의 데이터를 처리로 인해 또는 외부의 영향에 의해 응답시간이 길어지는 경우에 사용됩니다. 같은 시간내에 더 많은 일을 처리하여 응답시간을 단축하는 것이 주 목적입니다.

❌ 문제가 되는 경우

비동기 처리는 처리 시간 단축으로 인한 응답 시간 단축이라는 큰 장점이 있습니다. 큰 장점이 있는 만큼 사용할 때 주의해야 합니다.

1. 코드 복잡도

비동기처리에는 여러개의 스레드가 동시에 실행됩니다. 이를 관리하기 위해서는 코드가 복잡해집니다. 그만큼 디버깅에도 어려울 수 있으며 여러 개의 스레드가 동시에 접근하는 경우 같은 데이터를 처리하여도 결과가 일치하지 않는 경우가 발생할 수 있습니다.

2. 메모리 문제

스레드 풀에 의해 생성된 스레드가 다른 스레드와 동일한 메모리 공간을 공유합니다. 이로 인해 메모리 문제가 발생할 수 있습니다. 또한 스레드를 생성하고 관리하는데 일정한 오버헤드가 발생합니다. 이로 인해 처리량이 많은 일부 어플리케이션에서는 전체적인 성능이 떨어지는 경우도 발생할 수 있습니다.

비동기 처리를 사용하는 경우에 어떠한 문제점이 생길지 생각하며 적용해야 합니다.

👨‍💻 구현

controller

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class ApiController {
    private final AsyncService service;

    @GetMapping("/hello")
    public String hello() throws InterruptedException {
        log.info("init completableFuture");
        service.run();
        log.info("hello");
        return "hello";
    }
}

Service

@Slf4j
@Service
public class AsyncService {

    @Async
    public CompletableFuture run() throws InterruptedException {
        return CompletableFuture.completedFuture(hello());
    }

    public String hello() throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            log.info("thread sleep....");
            Thread.sleep(500L);
        }
        log.info("wake up!!");
        return "async hello";
    }
}

@Service는 스프링 프레임워크에서 비즈니스 로직을 수행하는 클래스에 대해 붙이는 어노테이션입니다. 해당 어노테이션을 사용하면 해당 클래스가 비즈니스 로직을 담당하는 서비스임을 명시적으로 표시할 수 있습니다. @Component와 유사하지만 @Service는 비즈니스 로직에 대한 Bean만 해당됩니다.

@Async를 사용하여 비동기처리 메서드로 실행할 수 있게 해주고 hello() 의 작업을 마치게 되면CompletableFuture를 반환합니다.

비동기 처리를 만약 하지 않았다면 init CompletableFuture -> thread sleep.... * 10 -> wake up!! -> hello 순으로 로그가 찍혀야 합니다. 또한 return 값을 받는데 Thread.sleep() 만큼 지연되게 됩니다.

비동기 처리를 하면 다음과 같이 로그가 나옵니다.

**실행결과

c.e.h.controller.ApiController :init completableFuture
c.e.h.controller.ApiController : hello
c.e.hopecoroutine.service.AsyncService : thread sleep....
c.e.hopecoroutine.service.AsyncService : thread sleep....
c.e.hopecoroutine.service.AsyncService : thread sleep....
c.e.hopecoroutine.service.AsyncService : thread sleep....
c.e.hopecoroutine.service.AsyncService : thread sleep....
c.e.hopecoroutine.service.AsyncService : thread sleep....
c.e.hopecoroutine.service.AsyncService : thread sleep....
c.e.hopecoroutine.service.AsyncService : thread sleep....
c.e.hopecoroutine.service.AsyncService : thread sleep....
c.e.hopecoroutine.service.AsyncService : thread sleep....
c.e.hopecoroutine.service.AsyncService : wake up!!

return이 되고 나서도 메서드는 실행이 되며 response 역시 Thread.sleep()의 영향을 받지 않았습니다.

위의 결과에서는 안보이지만 실제 실행을 하면 스레드의 이름까지 나오는 것을 확인할 수 있습니다. Task-1이런 식으로 나오게 되는데 이는 스프링에서 지정한 이름입니다.

스레드에 관한 내용은 다음에 따로 공부할 예정입니다.

✅ 요약

  • 비동기처리란 멀티 스레드 사용으로 작업을 분리 -> 응답을 받지 않고도 다른 작업을 먼저 처리
  • @Async,CompletableFuture를 주로 사용하며 자원을 효율적으로 사용하여 응답 속도 개선을 하기 위해 사용된다.
  • 주의할 점은 메모리 문제, 코드 복잡성 증가, 여러 스레드가 같은 데이터를 처리할 때 결과값이 달라질 수도 있기 때문에 주의하면서 사용해야한다.
profile
왜? 라는 질문이 사라질 때까지

0개의 댓글