Future을 이용한 비동기 프로그래밍

2023. 1. 1. 21:39자바멘토링

Future?

영수증이랑 같다!

ex)내가 햄버거가게를 가서 햄버거를 사면 영수증을 준다 거기에 적힌 번호로 기다리다가 나는 화장실 갔다오고

핸드폰을 하다 번호가 울리면 영수증을 가져가서 햄버거를 받아온다.

 

영수증은  크게 의미가 없으나 , 그걸 통해서 실제적인 객체로 반환을 받을 수 있다.

 

즉! Future은 비동기로 연산을 하고 그 결과를 가지고 있다.

제네릭 <>을 이용해서 내가 원하는 결과물을 얻을 수도 있다.

 

 

추가적으로 알아야 하는 용어!

Synchronous 

ex)요리사가 짜장면만 만든다 , 다른것은 만들지 않고 그 주문만 처리한다 ->

한가지의 목적만을 가지고 순서대로 해야할일 한가지만 한다. 라는 의미와 같다.

ASynchronous 

ex) 요리사가 짜장면만 만드는 것이 아니라 짬뽕도 만들고 탕수육도 동시에 만든다.->

음식이라는 것은 넓은 의미 안에 포함이 되지만 짬뽕 , 짜장면은 서로 다르다, 그런 것처럼 일을 하는데 있어서 

추구하는 목적이 다를 수 있고 동시에 일어나지 않을 수 있는 방식으로 일하는 것과 같다.

 

즉 여러 작업을 동시에 독립적으로 처리하는 방식이다.

 

동기 vs 비동기 

 

동기 : 요청과 응답이 같은 시간대 "안에 " 고정 되어 있다.

요청과 응답이 완료되기 까지 프로그램이 정지 되어있다.

 

비동기: 요청-응답간 결합이 자유로워 프로그램이 응답 받기 위해 대기하지 않고 자기 갈길을 간다

즉! 요청과 응답이 다른 시간대에 존재하기 때문에, 요청내용에 대해 지금 바로 당장 응답하지 않아도 된다.

 

 

JAVA의 비동기 기술

동기와 비동기 코드 작성 

 

동기코드 

public class FutureEx {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newCachedThreadPool();
        log.info("Hello");
        Thread.sleep(2000);
        log.info("Exit");
    }
}
[main] INFO com.example.demo.FutureEx - Hello
[main] INFO com.example.demo.FutureEx - Exit

 

 

#쓰레드 풀  -> 내가 원하는 쓰레드를 하나 새로만들면 되는데 그것을 만드는것은 cpu비용이나 
메모리에도 비용이 든다  , 동시에 10개밖에 안쓴다면  그것을 만들고 새로 작업시 작업이 끝난 쓰레드는 반납하고

그 쓰레드를 가져와서 다시 사용한다.

결과:메인 쓰레드로  순차적으로 위에서 부터 코드가 진행 되는 것을 알수 있다.

 

비동기코드 

 

 

execute

  • runnable 인터페이스 필요, result값 설정 가능
  • runnable - 객체를 리턴하지 않음, exception 발생시키지 않음, 반환 값 ,파라미터도 없다
    작업을 실행 시킨 이후에 별도의 작업으로 넘어갈수있다.
  • 고로, 객체를 리턴할 필요가 없을 때 사용한다.
  • shutdown() 걸어줘야 함
public class FutureEx {
    public static void main(String[] args) {
        ExecutorService es = Executors.newCachedThreadPool();
        es.execute(() -> {
            try {
                Thread.sleep(2000); //interrupt 발생시 exception 던질 수 있도록
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("Hello");
        });
        log.info("Exit");
    }
}
[main] INFO com.example.demo.FutureEx - Exit
[pool-1-thread-1] INFO com.example.demo.FutureEx - Hello

 

결과:  메인쓰레드와 쓰레드 풀에서 1개의 쓰레드가 더 만들어져 총 두개의 쓰레드로 작업이 진행되고 

동시에 작업 진행시  순서와는 상관없이 sleep이 2초 걸려있는 Hello가 늦게 출력 됨을 확인 할 수있다.

 

 

submit

특징

  • runnable, callable 인터페이스 사용 가능
  • callable - 값 리턴 가능, exception 발생시킬 수 있음
  • 고로, 객체 리턴이 필요하거나 exception 발생이 필요할 때 사용한다.
public class FutureEx {

    public static  void main(String[] args) throws InterruptedException, ExecutionException {
        //쓰레드 풀 생성
        ExecutorService es = Executors.newCachedThreadPool();

        Future<String> f = es.submit(()->{//리턴 값을 받겠다.(핸들러 같은것)
            Thread.sleep(2000);
            System.out.println("Async");
            return "Hello";//비동기 작업이 결과를 리턴한다.
        });
        System.out.println(f.isDone());//즉시리턴:결과 처리 여부 확인 
        Thread.sleep(2100);
        System.out.println(f.get());//Blocking, Non-Blocking 대기여부
        System.out.println("Exit");


    }

}
false
Async
Hello
Exit

결과: 아직 결과 처리가 되지 않았기에 가장 먼저 isDone()함수가 호출이 된다.

그다음에 get()함수는  future에 값이 저장 될때까지 2초 기다렸다가 반환값을 받고 나서  출력을 한다.

그동안 Exit는 실행될수 없다 , 이것은 get()메소드가 결과값을 받을때 까지 대기하는  블로킹이기 때문에  반환값을 리턴 받고 나서  출력이 가능하다.

 

 

Java Future와 FutureTask, Callback

submit으로 리턴 받은 비동기 수행 결과값을 저장할 때 Future와 Callback을 사용한다

 

 

 FutureTask이용한 카운터 예제 코드

 

package hello.hellospring.toby;


import java.util.concurrent.*;


public class FutureEx1 {

    public static  void main(String[] args) throws InterruptedException, ExecutionException {
        //쓰레드 풀 생성
        ExecutorService es = Executors.newCachedThreadPool();



        FutureTask<String> f = new FutureTask<String>(()->{//리턴 값을 받겠다.(핸들러 같은것)
            int counter=1;

            for(int i =0;i<=10 ;i++) {
                Thread.sleep(1000);
                counter +=1;
                System.out.println(counter);

            }
            return String.valueOf(counter);//비동기 작업이 결과를 리턴한다.

        });
        es.execute(f);
        System.out.println("Exit");
        System.out.println(f.isDone());//처리 여부 (while문 돌면서 확인할때 사용)
        System.out.println( f.get());//Blocking, Non-Blocking 대기여부



    }

}
Exit
false
2
3
4
5
6
7
8
9
10
11
12
12

결과: 쓰레드 두개로 "Exit "가 먼저 출력이 된후에  처리가 되었는지 확인하는 false가 즉시 리턴 된다 그다음에  for문이 돌고  마지막에 couter에 있는 값을 가지고 온 후에 get()함수가 출력이 된다.

 

Callback

  • Callback을 이용하여 비동기 실행 결과를 처리할 수 있는 코드
  • try/catch 작성이 빈번한 Future보다 더 우아한 코드라고 할 수 있다..!
  • 더 나은 방법이 있지만, 기본기 중에서는 콜백 기법이 더 나음
public class FutureEx {
    interface SuccessCallback {
        void onSuccess(String result);
    }
    interface  ExceptionalCallback{
        void onError(Throwable t);
    }
    public static class CallbackFutureTask extends FutureTask<String> {
        SuccessCallback sc;
        ExceptionalCallback ec;
        public CallbackFutureTask(Callable<String> callable, SuccessCallback sc, ExceptionalCallback ec) {
            super(callable);
            this.sc = Objects.requireNonNull(sc); //Tip. Null이 들어오면 안될 때 사용하는 메서드
            this.ec = Objects.requireNonNull(ec);
        }

        @Override
        protected void done() {
            try {
                sc.onSuccess(get());
            } catch (InterruptedException e) {
               Thread.currentThread().interrupt();
            } catch (ExecutionException e) {
                ec.onError(e.getCause());
            }
        }
    }

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

        CallbackFutureTask f = new CallbackFutureTask(() -> {
            Thread.sleep(2000);
            if(1==1) throw new RuntimeException("Async ERROR!!!");
            log.info("Async");
            return "Hello";
        },
            s -> System.out.println(s),
            e-> System.out.println("Error: "+e.getMessage()));

        es.execute(f);
        es.shutdown(); //이 메서드를 쓰더라도 하던 작업이 중단되지는 않음
    }
}

 

 

그럼 콜백함수란 무엇일까?
"요청 처리가 완료되어 (다시) 사용하세요 !" 라는 신호라고 생각합니다.
즉, 콜백함수에서 콜백이란 이름이 별로 있는 것이 아니라, 함수(function)가 있는데, 사용되는 목적/용도가 "call + back" 이라는 것입니다
(아직도 그 뜻을 정확히는 모르지만 대충은 이런 느낌이지 않을까 생각한다는 의미입니다.)

즉. 콜백함수가 실행됐다는 것으로 요청한 작업이 끝났음을 알리고, 작업의 결과물을 콜백함수를 통해 사용가능하게 됩니다.

사용 된 예로, 클릭 이벤트가 발생할 때 출력되는 콜백 함수를 본적이 있을 것입니다.

콜백헬(hell), 피라미드 무덤

콜백함수가 늘어나면 늘어날 수록 코드의 깊이가 늘어나 더이상 헤어날 수 없다는 유머 입니다.
코드 관점에서 콜백함수는 중첩으로 들여쓰기(nesting)가 반복되 가독성이 떨어지게 됩니다.이런 중첩된 모습이 꼭 피라미드 같다고 해서 피라미드 무덤 이라고 합니다.
무덤은 빠지면 헤어나올 수 없는 곳이죠?

Promise 도입목적

콜백헬, 피라미드 무덤 처럼 콜백함수로 꼬여버린 함수에서
우리를 구원할 새로운 개념이 도입됐습니다.
그것은 Promise 입니다.

Promise가 도입 된 것이 비단 그런 이유뿐일까요,

  • 첫째. 비동기 처리는함수에서 처리된 결과값을 반환할 경우, (비동기)함수에서 찾을 수 밖에 없어 코드가 복잡할 경우 어려움이 있습니다. Promise는 구조가 간단해 반환값을 찾아 사용하기 쉽습니다.
  • 둘째. 비동기 처리를 위한 콜백패턴이 처리순서를 보장하지 않습니다.
  • 셋째. 에러처리에 대한 한계가 있습니다.
    에러는 호출자 방향으로 전파되는데 만약 호출자가 사라진다면 어떨까요?
    다시 말해 비동기처리 함수는 콜백함수를 호출하고 그것이 완료되기 전에 바로 호출스택을 빠져나갑니다. (콜백)함수를 호출한 호출자가 사라지는 겁니다.
    만약 비동기처리 함수에서 에러가 발생할 경우. catch 구문에서 에러케치가 어려워 프로세스가 종료 될 수도 있습니다.

Promise는

promise로 구현된 비동기 처리 함수는
콜백을 예측 가능한 패턴으로 사용하도록 도와주며,
콜백함수 안에서 생성된 프로미스 객체를 활용해 콜백함수가 성공,실패,오류 각각의 경우에 따라 후속 처리를 할 수 있습니다.
순차적이지 않는 비동기함수의 실행순서를 제어할 수 있게 도와줍니다.
콜백패턴에 비해 코드 가독성이 좋고 반환된 결과물을 사용하기 편합니다.

 

참고:[Async, 비동기와 동기] Promise에서 Await까지 (velog.io)

[Java] 비동기 기초 Future, Callback (tistory.com)

https://www.youtube.com/watch?v=oFXV4qSXNVs

 

[Java] 비동기 기초 Future, Callback

JAVA의 비동기 기술 본 정리는 토비의봄 8회 리액티브 프로그래밍(4) 자바와 스프링의 비동기 기술을 토대로 정리한 내용입니다. (무려 10년 전 사용하던 기술....현재는 더 좋은 방법이 존재하나

heekim0719.tistory.com

 

[Async, 비동기와 동기] Promise에서 Await까지

비동기/동기에 대한 정의와 콜백함수,콜백지옥을 거쳐 Promise가 도입된 이유 그리고 간략히 Async Await까지 순서대로 정리하려고 합니다. 코드 보다는 개론적인 의미에 치중해 작성된 글입니다. Pro

velog.io

 

'자바멘토링' 카테고리의 다른 글

TCP/IC  (0) 2023.01.06
리플렉션을 이용한 json 직렬화  (0) 2022.12.28
파일디스크립터  (0) 2022.12.18
리플렉션  (0) 2022.12.15
Try - With - Resource  (0) 2022.12.04