자바 비동기 (1)

· ☕ 5 min read · 👀... views

최근 학생들에게 학습을 추천해주는 것과 관련된 작업을 하고 있는데요.

주 단위로 추천이 되어 매주 배치를 통해서 추천 학습이 들어가지만, 신규 회원일 경우 배치에서 누락되는 경우가 있어서, 로그인 등의 행위를 했을 때 비동기적으로 데이터를 쌓아줄까 고민을 하고 있습니다.

프로젝트는 Java - Spring 을 기반으로 하고 있는데, 잠깐만 찾아봐도 방법이 많습니다.

이번 글에서는 비동기에 대한 정의 및 올드한 내용에 대해 다뤄볼까 합니다.

1. 비동기 프로그래밍이란?

일단, 비동기를 논하기 전에 왜 비동기가 나왔는지부터 알아야 할 것 같습니다.

기본적으로 메소드를 실행한다는 것은 메소드가 완료될 때까지 그 메소드를 호출한 코드는 기다려야 함(블록킹 blocking) 을 의미합니다. 얼핏 들으면 당연해보입니다. 결과가 나와야 그 결과를 가지고 뭘 할테니까요.

하지만, 그렇지 않아야 할 때도 있습니다. 가령 쇼핑몰에서 상품을 사기 위해서 상품 목록을 보는데, 상품 목록을 전부 가져올 때까지 기다린다고 생각해보세요. 사용자는 통신이 완료될 때까지 로딩 중이라는 하얀 화면만 봐야할테고, 대부분의 사용자는 기다리지 않고 나갈테죠.

이런 경우 떄문에 우리는 UI 쪽에서는 주로 AJAX(Asynchronous Javascript and XML) 를 통해 위와 같은 문제를 해결합니다.

또 다른 경우라면, 서버에서 사용자의 로그를 쌓을 때도 비동기로 할 수 있겠네요.

비동기 프로그래밍은 기본적으로 Fire-and-Forget (저지르고 잊기) 입니다, 즉 작업을 붙잡지 않고 흘려보낸다음 결과가 준비되면 그제야 가져옵니다.

서로 함께 움직일 필요가 없는 작업을 분리할 때 좋은 선택지라고 할 수 있습니다.

2. Thread, Runnable

취준생 시절, 비동기에 대한 질문이 오면 항상 나왔던 내용이 바로 Thread 와 Runnable 에 대한 내용이었는데요.

자바에서는 위의 두 클래스와 인터페이스를 각각 상속, 구현하는 형태로 비동기 코드를 간단하게 실행할 수 있습니다.

1
2
3
4
5
6
7
public class AsyncRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("run!");
    }
}
1
2
3
4
5
6
public class HelloThread extends Thread {

    public void run() {
        System.out.println("run!");
    }
}
1
2
3
4
5
6
7
// 실행
new Thread(new AsyncRunnable()).start();

AsyncRunnable asyncRunnable = new AsyncRunnable();
asyncRunnable.run();

new HelloThread().start();

위의 Thread 를 다루는 메소드도 존재하는데요.

대표적인 메소드가 sleep() 입니다.

이런 메소드들은 간략하게 살펴보면

  1. sleep(mills) : 지정한 시간동안 쓰레드를 잠재웁니다.
  2. join() : 쓰레드를 동기(?) 적으로 처리할 수 있습니다. 예를 들어 t2 의 값을 t1 이 받아서 작업을 해야 할 경우 t2.join() 을 통해 t2의 값을 가져와서 t1이 마저 작업을 할 수 있습니다.
  3. yield() : 실행 중인 쓰레드가 동일 또는 높은 우선순위 쓰레드에게 양보할 때 사용합니다.

3. Future

자바 1.5에서 등장한 비동기 계산의 결과를 나타내는 interface 입니다.

보통 어노테이션을 붙여 비동기로 실행할 방법이 마땅치 않아 프레임워크의 일부인 ExecutorService 와 함께 엮어 사용합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class FutureService {

    public void future() throws ExecutionException, InterruptedException {

        ExecutorService executorService = Executors.newSingleThreadExecutor();

        Future<String> reference = executorService.submit(
                new Callable<String>() {
                    @Override
                    public String call() throws Exception {
                        return "Hello";
                    }
                }
        );
        
        if(reference.isDone()) {
            System.out.println(reference.get());
        }
    }
}

값을 리턴하기 위해 callable 인터페이스를 사용하였고, 값을 가져오기 위해 get() 메소드를 사용한 것을 알 수 있습니다.

하지만, get() 메소드를 호출하면 비동기 작업이 완료될 때까지 해당 쓰레드가 블록킹이 되며, 이미 완료되었는지 확인하는 isDone() 가 있는 것을 확인 할 수 있습니다.

4. FutureTask

Future 인터페이스를 구현한 클래스로 러너블 인터페이스를 구현해서 직접 실행시킬 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class FutureService {

    public void futureTask() {

        ExecutorService executorService = Executors.newSingleThreadExecutor();

        FutureTask<String> reference = new FutureTask<>(
                new Callable<String>() {
                    @Override
                    public String call() throws Exception {
                        return "Hello";
                    }
                }
        );

        executorService.execute(reference);
        //reference.cancel(true);
    }
}

위의 Future 와 비교하면 executorService.execute 를 통해 직접 실행시킬 수 있는 것을 알 수 있습니다.

실행을 취소하려면 cancel(boolean mayInterruptIfRunning) 메소드를 호출하면 됩니다.

제가 참고한 책에서는 자바EE에서는 @Asynchronous 라는 어노테이션이 사용했습니다만, 확인해보니 해당 어노테이션이 있는 javax.ejb 패키지가 1.7(java7) 이후로는 deprecated 된 패키지 이므로 java8 이전에는 그런 것도 있었구나 하고 넘어가면 될 것 같습니다.

5. 비동기 서블릿

아마 요즘 환경에서는 websocket, socket.io 등의 기술 덕분에 쓰지 않을 방법이겠지만, 공부차 이 방법도 한 번 짚고 넘어가려고 합니다.

(실은 요즘은 2~4번 방법도 안 쓰지 않을까 싶어요.)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@WebServlet(urlPatterns = {"/async"}, asyncSupported = true)
public class AsyncServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        final AsyncContext asyncContext = req.startAsync();

        asyncContext.addListener(new AsyncListener() {
            @Override
            public void onComplete(AsyncEvent asyncEvent) throws IOException {
                // ...
            }

            @Override
            public void onTimeout(AsyncEvent asyncEvent) throws IOException {
                // ...
            }

            @Override
            public void onError(AsyncEvent asyncEvent) throws IOException {
                // ...
            }

            @Override
            public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
                // ...
            }
        });
    }
}

startAsync() 를 통해 비동기 작업이 가능하며, AsyncListener 에는 4개의 메소드가 존재하는데, 각각 살펴보면

  • onComplete() : 실행이 끝날 때 한 번만 실행
  • onTimeout() : 타임아웃이 발생할 때만 실행
  • onError() : 에러를 받았을 때만 실행
  • onStartAsync() : 비동기 컨텍스트가 시작될 때 실행

이름만 보면 어떤 행동을 할지 알 수 있습니다. (이름의 중요성…)

다른 방법으로는 ExecutorService 에 위임해주는 방법인데요. 살펴보면

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@WebServlet(urlPatterns = {"/async"}, asyncSupported = true)
public class AsyncServlet extends HttpServlet {
    
    @Resource
    private ExecutorService executorService;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        AsyncContext asyncContext = req.startAsync();
        final PrintWriter writer = resp.getWriter();
        
        executorService.submit(() -> {
            writer.println("end");
            asyncContext.complete();
        });
    }
}

쓰레드 생성/시동을 ExecutorService 에 맡기고, 서블릿 관련 코드만 처리하는 것으로,

Future 를 사용했을 때와 비슷하게 사용할 수 있는 것을 알 수 있습니다.

6. 정리

비동기 작업은 실행이 다 끝난 후 응답을 반환하는 거의 모든 경우에 쓰입니다. 비동기 작업을 하면 하위 작업을 다른 쓰레드에 맡김으로써 응답 시간을 단축시킬 수 있습니다.

하지만, 비동기 실행을 하면 쓰레드를 새로 만들어야 하기 때문에 JVM 은 비동기 메소드 갯수만큼 컨텍스트 스위칭 횟수가 늘어납니다. 컨텍스트 스위칭이 너무 잦아지면 쓰레드가 바닥나거나 성능이 외려 동기 실행보다 못할 수도 있다고 합니다. (책 여러 권을 병렬로 읽는다고 생각해보세요. 혹은 프로젝트 6개를 병렬로 처리한다거나…) 그리고, 실행 순서를 보장할 수 없기 때문에 디버깅이 어렵다는 단점도 있네요.

그렇기에 원리를 잘 알고 사용해야 함에는 틀림 없는 것 같습니다. 관련 기술도 많구요.

다음에는 java8에서 생긴 CompletableFutre, Spring 의 @Async 등 좀 더 근대적인(?) 방법에 대해 소개해볼까 합니다.

Share on

snack
WRITTEN BY
snack
Web Programmer


What's on this Page