728x90

자바에서 비동기 프로그래밍을 하기 위해 알아야 하는 Future 인터페이스에 대해 알아보자.

 

Method reference

:: 연산자를 이용해서 함수에 대한 참조를 간결하게 포현한 것이다.

package org.example;

import java.util.function.Consumer;
import java.util.stream.Stream;

public class Main {

    public static class Student {
        private final String name;

        public Student(String name) {
            this.name = name;
        }

        public boolean compareTo(Student student) {
            return student.name.compareTo(name) > 0;
        }

        public String getName() {
            return name;
        }
    }

    public static void print(String name) {
        System.out.println(name);
    }

    public static void main(String[] args) {
        var target = new Student("f");

        Consumer<String> staticPrint = Main::print;

        Stream.of("a", "b", "k", "z")
                .map(Student::new)
                .filter(target::compareTo)
                .map(Student::getName)
                .forEach(staticPrint);
    }
}

위의 코드를 예시로 들면

method reference: target::compareTo

static method reference: Main::print

instance method reference: Student::getName

constructor method reference: Student::new

이렇게 해당된다.

 

ExecutorService

쓰레드 풀을 이용하여 비동기적으로 작업을 실행하고 관리해준다.

쓰레드를 생성하고 관리하는 작업이 필요하지 않기 때문에, 코드를 간결하게 유지가 가능하다.

public interface ExecutorService extends Executor {
    void shutdown();

    <T> Future<T> submit(Callable<T> task);

    void execute(Runnable command);
}

이렇게 구성이 되어 있으며 각 메서드는 다음과 같이 동작한다.

execute: Runnable 인터페이스를 구현한 작업을 쓰레드 풀의 쓰레드에서 비동기적으로 실행한다.

submit: Callable 인터페이스를 구현한 작업을 쓰레드 풀에서 비동기적으로 실행하고, 해당 작업의 결과를 Future<T> 객체로 반환한다.

shutdown: ExecutorService를 종료하며, 더 이상의 task를 받지 않는다.

 

Executors를 사용하여 ExecutorService를 생성한다.

  • newSingleThreadExecutor: 단일 쓰레드로 구성된 쓰레드 풀을 생성, 한 번에 하나의 작업만 실행
  • newFixedThreadPool: 고정된 크기의 쓰레드 풀을 생성. 크기는 인자로 주어진 n과 동일
  • newCachedThreadPool: 사용가능한 쓰레드가 없다면 생성, 있다면 재사용하며 쓰레드가 일정시간 사용되지 않으면 회수
  • newScheduledThreadPool: 스케줄링 기능을 갖춘 고정 크기의 쓰레드 풀을 생성. 주기적이거나 지연이 발생하는 작업을 실행
  • newWorkStealingPool: work steal 알고리즘을 사용하는 ForkJoinPool을 생성

 

 

Future

이제 Future 인터페이스에 대하여 자세히 살펴보자.

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future 인터페이스는 이렇게 구성되어 있다.

 

  • isDone: task가 완료되었다면, true 반환
  • isCancelled: task가 cancel에 의해 취소된 경우, true 반환
  • get: 결과를 구할 때까지 thread가 계속 block(future가 오래 걸린다면 thread가 blocking 상태 유지), 이 문제를 해결하기 위해 timeout의 인자를 받는 메서드가 존재
  • cancel: future의 작업을 취소하며, 취소할 수 없는 상황이면 false를 반환한다. mayInterruptIfRunning이 false라면 시작하지 않은 작업만 취소

 

이렇게 Future에 대해서 알아보았는 데, cancel로 정지시키는 거 말고는 future를 컨트롤 할 수가 없다.

또한 반환된 결과를 get으로 기다린 후 접근하기 때문에 비동기로 작업하기가 어렵다.

728x90

스프링을 공부하기 전에 항상 나오는 말이 있다.

스프링은 동기, node 서버는 비동기라는 말을 굉장히 많이 들었던 것 같다.

처음에 공부할 때는 외우고만 있다가 해당 내용을 운영체제에서 공부하고 그때서야 이해할 수 있게 되었다.

 

이번에도 공부하기 전에 동기와 비동기에 대해서 정리하고 가고자 한다.

 

Caller와 Callee

프로그래밍에서 굉장히 많이 봤던 용어일 것이다.

caller는 말 그대로 다른 함수를 호출하는 함수이고, callee는 그때에 호출당하는 함수이다.

 

함수형 인터페이스

1개의 추상 메서드를 가지고 있는 인터페이스 함수를 1급 객체로 사용하는 것을 말한다.

자바에서는 람다 표현식을 이 함수형 인터페이스에서만 사용 가능하다.

package org.example;

@FunctionalInterface
public interface Function<T> {

    void accept(T t);
}

 

해당 함수형 인터페이스를 사용해보도록 하자.

package org.example;

public class Main {

    public static void main(String[] args) {
        var function = getFunction();
        function.accept(1);

        var functionAsLambda = getFunctionAsLambda();
        functionAsLambda.accept(1);

        functionHandler(function);
    }

    public static Function<Integer> getFunction(){
        Function<Integer> returnValue = new Function<Integer>() {
            @Override
            public void accept(Integer integer) {
                myLog("value in interface: " + integer);
            }
        };
        return returnValue;
    }

    public static Function<Integer> getFunctionAsLambda(){
        return integer -> myLog("value in lambda: " + integer);
    }

    public static void functionHandler(Function<Integer> function){
        myLog("functionHandler");
        function.accept(1);
    }


    public static void myLog(String string){
        System.out.println(Thread.currentThread() + " -> " +string);
    }
}

 

이 코드를 실행해보면 

 

이때 모두 메인 스레드에서 실행이 되는 것을 볼 수 있다.

 

함수 호출 관점

 

A 프로그램

package org.example;

public class programA {

    public static void myLog(String string){
        System.out.println(Thread.currentThread() + " -> " +string);
    }

    public static void main(String[] args) {
        myLog("start main");
        var value = getValue();
        var nextValue = value + 1;
        myLog(String.valueOf(value == nextValue));
        myLog("Finish main");
    }

    public static int getValue(){
        myLog("start getValue");
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }

        var value = 0;
        try{
            return value;
        }finally {
            myLog("Finish getResult");
        }
    }
}

 

이 모델의 실행을 그림으로 표현하면 다음과 같다.

main 함수는 getValue의 실행이 끝날 때까지 기다린 후 해당 데이터를 가지고 작업을 한다.

 

이것과는 다른 B 프로그램이다.

package org.example;

public class programB {

    public static void myLog(String string){
        System.out.println(Thread.currentThread() + " -> " +string);
    }

    public static void main(String[] args) {
        myLog("start main");
        getValue(integer -> {
            var nextValue = integer + 1;
            myLog(String.valueOf(nextValue == 1));
        });

        myLog("Finish main");
    }

    public static void getValue(Function<Integer> function){
        myLog("Start getValue");
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }

        var value = 0;
        function.accept(value);
        myLog("Finish getValue");
    }
}

 

해당 프로그램은 main에서 직접 실행하는 것이 아니라 getValue에서 실행하도록 한다.

 

이 프로그램을 그림으로 나타내면 다음과 같다.

 

두 프로그램의 차이점은
A 프로그램은 다음 코드를 위해 callee의 반환값이 필요하다.

B 프로그램은 다음 코드를 위해 callee의 반환값이 필요하지 않고 callee가 결과를 이용해서 callback을 수행한다.

 

이러면 A 프로그램을 동기, B 프로그램을 비동기라고 생각을 하면 된다.

 

+ Recent posts