[Reactive Java #3] 자바 비동기 프로그래밍

YoungHo-Cha·2022년 10월 2일
3

Web Flux

목록 보기
4/6
post-thumbnail

이번에는 자바 비동기 프로그래밍을 체험해보자.

목차

  • 동기코드
  • 비동기 코드 V1
  • Future V2
  • Callback V3
  • FutureTask V4

동기 코드

먼저 동기 코드를 살펴보자.

public class SyncProgramming {

    public static void main(String[] args) throws InterruptedException {

        Thread.sleep(2000);
        System.out.println("start --- Thread = " + Thread.currentThread().getName());

        System.out.println("exit --- Thread = " + Thread.currentThread().getName());
    }
}

결과는 다음과 같다.

당연히 start -> exit 순서로 동작할 것이다.

하지만 이러한 작업은 동기적이다.

그래서 차근차근 비동기적으로 만들어보자.
(exit가 먼저 수행되어야 함)

비동기 코드 V1

먼저 동기코드를 작성해보자.

다음 코드를 보자.

public class AsyncProgramming {

    public static void main(String[] args) {
        ExecutorService es = Executors.newCachedThreadPool();

        es.execute(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }

            System.out.println("start");
        });

        System.out.println("exit");
    }
}

실행결과는 다음과 같다.

분명 exit 코드가 나중에 실행되어야하는데, 먼저 출력된 것을 볼 수 있다.

exit 코드가 먼저 실행된 이유는 새로운 쓰레드를 할당받아서 수행하였기 때문에 main 쓰레드는 끝났고 새로운 쓰레드에서 수행한 것을 볼 수 있다.

코드를 다음과 같이 수정하고 쓰레드 정보를 살펴보자.

public class AsyncProgramming {

    public static void main(String[] args) {
        ExecutorService es = Executors.newCachedThreadPool();

        es.execute(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }

            System.out.println("start --- Thread name = " +  Thread.currentThread().getName());
        });

        System.out.println("exit --- Thread name = " + Thread.currentThread().getName());
    }
}

결과는 다음과 같다.

exit 는 main 쓰레드에서 동작하고, start 는 새로운 쓰레드에서 동작한 것을 볼 수 있다.

하지만 여기에는 치명적인 오류가 있다.
우리는 비동기 프로그래밍으로, main에서 새로운 쓰레드에서 동작한 결과값을 받고 싶은 것이다.

이를 위해서 자바에서는 "Future"이라는 객체를 지원한다.

별도의 쓰레드에서 비동기적으로 동작하는 값을 메인쓰레드로 넘겨서 리턴해보자.

Future

먼저 다음의 코드를 살펴보자.

public class AsyncProgrammingV2 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newCachedThreadPool();

        Future<String> f =  es.submit(() ->{
            Thread.sleep(2000);
            System.out.println("start --- Thread = " + Thread.currentThread().getName());
            return "finish";
        });
        System.out.println("결과 값 = " + f.get() + " --- Thread = " + Thread.currentThread().getName()); // blocking
        System.out.println("exit -- Thread = " + Thread.currentThread().getName());
        es.shutdown();

    }
}

결과는 다음과 같다.

쓰레드에서 결과 값을 받아서 main 쓰레드에서 출력한 것을 볼 수 있다.

문제

하지만 여기서도 문제가 존재한다.

문제는 exit가 먼저 수행되지 않고, 쓰레드가 끝난다음 수행된 것을 볼 수 있다.

맞다. f.get() 메소드는 blocking 상태인 것을 볼 수 있다.

그러면 다음과 같이 코드를 작성하면 될 것이다.

public class AsyncProgrammingV2 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newCachedThreadPool();

        Future<String> f =  es.submit(() ->{
            Thread.sleep(2000);
            System.out.println("start --- Thread = " + Thread.currentThread().getName());
            return "finish";
        });
        System.out.println("exit -- Thread = " + Thread.currentThread().getName());
        System.out.println("결과 값 = " + f.get() + " --- Thread = " + Thread.currentThread().getName()); // blocking
        es.shutdown();

    }
}

결과는 다음과 같다.

이러한 점을 알고 다음과 같이 코딩을 하면 f.get()을 기다리는 동안 다른 것을 수행할 수 있을 것이다.

다음 코드를 보자.

blocking 상태에 다른 일 하기

public class AsyncProgrammingV3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newCachedThreadPool();

        Future<String> f = es.submit(() ->{
            Thread.sleep(8000);
            System.out.println("start --- Thread = " + Thread.currentThread().getName());
            return "finish";
        });

        System.out.println("exit --- Thread = " + Thread.currentThread().getName());

        while(!f.isDone()){
            System.out.println("do another task~ --- Thread = " + Thread.currentThread().getName());
            Thread.sleep(2000);
        }
        System.out.println("결과 값 = " + f.get() + " --- Thread = " + Thread.currentThread().getName());
        es.shutdown();
    }
}

이 구현은 "스핀락"과 매우 흡사하다.

결과는 다음과 같다.

f.isDone()을 통해서 지속적으로 쓰레드의 상태를 기다리면서 다른 작업을 한 것을 볼 수 있다.

이 방법은 좋지 않다고 판단된다. 지속적으로 isDone을 통해서 상태를 확인해야하고, 다른 일을 수행하는 것 또한 blocking 작업들이 있을 수 있기 때문에 코딩하면서 생각해야하는 문제들이 너무나 많다.

이 방법도 있지만 callback 메서드를 이용하는 방법도 존재한다.


Callback

다음의 코드를 보자.

public class AsyncProgrammingV4 {

    public static void main(String[] args) {
        ExecutorService es = Executors.newCachedThreadPool();

        es.submit(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            System.out.println("start --- Thread = " + Thread.currentThread().getName());
            callback("finish --- Thread = " + Thread.currentThread().getName());
        });

        System.out.println("exit --- Thread =" + Thread.currentThread().getName());
        es.shutdown();
    }

    public static void callback(String str){
        System.out.println("작업 끝 --- Thread = " + Thread.currentThread().getName());
    }
}

생성된 Thread가 작업을 완료하면 static 메서드를 실행한 것을 볼 수 있다.

결과는 다음과 같다.

오류

이 방법 또한 오류가 존재한다.

  1. callback을 static으로 다루어야 한다.
  2. 따로 method를 지정하여 관리하여야 한다.

위의 2가지 요소가 존재하여 개발자가 개발하는데 불편하다.

이를 위해서 자바는
FutureTask를 지원한다.


FutureTask

FutureTask는 별도의 "done"이라는 메소드를 오버라이딩하여, task가 완료되었을 때 콜백 함수를 실행할 수 있도록 해준다.

다음의 코드를 보자.

public class AsyncProgrammingV5 {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newCachedThreadPool();

        FutureTask<String> f = new FutureTask<String>(() ->{
            Thread.sleep(1000);
            System.out.println("start --- Thread = " + Thread.currentThread().getName());
            return "finish";
        }){
            @Override
            protected void done() {
                try {
                    System.out.println(get() + " --- Thread = " + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                } catch (ExecutionException e) {
                }
            }
        };

        es.execute(f);

        System.out.println("exit --- Thread = " + Thread.currentThread().getName());

        es.shutdown();

    }
}

결과는 다음과 같다.

profile
관심많은 영호입니다. 궁금한 거 있으시면 다음 익명 카톡으로 말씀해주시면 가능한 도와드리겠습니다! https://open.kakao.com/o/sE6T84kf

0개의 댓글