반응형

1. 서론

이번 포스팅에서는 Chapter16의 CompletableFuture : 안정적 비동기 프로그래밍에 대해 진행하도록 하겠습니다.

 

2. Future의 단순 활용

Future 인터페이스는 비동기 계산을 모델링 하는데 쓰이며, 계산이 끝났을 때 결과에 접근할 수 있는 참조를 제공합니다.

 

Future 이용시에는 시간이 오래걸리는 작업을 Callable 객체 내부로 감싼 다음 ExecutorService에 제출하면 됩니다.

 

아래는 예제 코드입니다.

 

ExecutorService executorService = Executors.newCachedThreadPool();
Future<Double> future = executorService.submit(new Callable<Double>() {
    Override
    public Double call() throws Exception {
        return doSomeThingLongComputation();
    }
});        
doSomeThingElse();        
try {
    double result = future.get(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
    // 현재 스레드에서 대기 중 인터럽트 발생
} catch (ExecutionException e) {
    // 계산 중 예외
} catch (TimeoutException e) {
    // Future가 완료되기 전에 타임아웃 발생 
}

 

위 예제에서 doSomeThingLongComputation 연산은 별도 쓰레드에서 수행하게 됩니다.

수행 후, 값을 얻기 위해 get 메서드를 호출해야합니다.

연산이 끝난 경우에는 바로 반환되지만 연산이 진행중이라면 get 메서드에서 블로킹 됩니다.

 

1) Future의 제한

 

Future는 일련의 동시 실행 코드를 구현하기에 충분하지 않습니다.

 

예를들어, '오래 걸리는 A라는 계산이 끝나면 그 결과를 다른 오래 걸리는 계산 B로 전달하시오 그리고 B의 결과가 나오면 다른 질의의 결과와 B의 결과를 조합하시오' 와 같은 요구사항을 쉽게 구현하기가 어렵다는 점입니다. 

 

이러한 제한적인 부분을 자바 8에서 제공하는 CompletableFuture로 해결할 수 있습니다.

 

3. 비동기 API 구현

CompletableFuture를 이용하여 최저가격 검색 어플리케이션 예제를 진행하겠습니다.

 

public class Shop {
    
    private static final Random random = new Random();
    
    private String name;

    public Shop(String name) {
        this.name = name;
    }
    
    public double getPrice(String product) {
        return calculatePrice(product);
    }
    
    private double calculatePrice(String product) {
        delay();
        return  random.nextDouble() * product.charAt(0) + product.charAt(1);
    }
    
    private static void delay() {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

위 예제에서는 상품명을 받아 1초의 작업 후 연산 결과를 반환하는 getPrice 메서드가 있습니다.

 

 

1) 동기 메서드를 비동기 메서드로 변환

 

위 예제의 경우 getPrice를 호출한 쓰레드는 1초 간 블로킹 되어집니다.

 

이 부분을 해소하기 위해 CompletableFuture를 이용한 getPriceAsync 메서드를 추가하겠습니다.

 

public Future<Double> getPriceAsync(String product) {
    CompletableFuture<Double> futurePrice = new CompletableFuture<>();
    new Thread(() -> {
       double price = calculatePrice(product);
       futurePrice.complete(price);
    }).start();
    
    return futurePrice;
}
Shop shop = new Shop("BestShop");
long start = System.nanoTime();
Future<Double> futurePrice = shop.getPriceAsync("my favorite product");
long invocationTime = ((System.nanoTime() - start) / 1000000);
System.out.println("Invocation returned after " + invocationTime + "msecs");

doSomeThingElse(); // 가격 정보를 가져오는 동안 다른 일 수행
        
try {
    double price = futurePrice.get();
    System.out.printf("Price is %.2f%n", price);
} catch (Exception e) {
    throw new RuntimeException(e);
}
        
long retrievalTime = ((System.nanoTime() - start) / 1000000);
System.out.println("Price returned after " + retrievalTime + " msecs");

 

위 코드의 getPriceAsync는 가격 계산이 끝나기 전에 return을 받습니다.

단, doSomeThingElse 의 작업이 먼저 끝난다면 get 메서드시 블로킹 되는 문제는 여전히 가지고 있습니다.

 

이 문제는, CompletableFuture의 기능을 활용하면 해결 가능하며 아래에서 더 살펴보겠습니다.

 

 

2) 에러 처리 방법

 

비동기로 처리시에는 별도의 쓰레드에서 동작하기에 에러 처리가 매우 까다롭습니다.

 

하지만, CompletableFuture 의 completeExceptionally 메서드를 사용하면 매우 간단해집니다.

 

아래는 예제 코드입니다.

 

public Future<Double> getPriceAsync(String product) {
    CompletableFuture<Double> futurePrice = new CompletableFuture<>();
    new Thread(() -> {
        try {
            double price = calculatePrice(product);
            futurePrice.complete(price);     
        } catch (Exception e) {
            futurePrice.completeExceptionally(e);
        }
    }).start();

    return futurePrice;
}

 

위와 같이 코드 추가 후 예외가 실제로 발생되면, 클라이언트는 ExecutionException을 받게 됩니다.

 

 

3) 팩토리 메서드 supplyAsync로 CompletableFuture 만들기

 

위 CompletableFuture를 만드는 코드가 매우 긴게 가독성이 좋지 않습니다.

 

이를 위해, CompletableFuture는 팩터리 메서드로 supplyAsync를 제공합니다.

 

위 getPriceAsync 메서드를 팩터리 메서드 supplyAsync를 적용한 코드는 아래와 같습니다.

 

public Future<Double> getPriceAsync(String product) {
   return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}

 

이 방법은 위 에러처리 방법에서 본 completeExceptionally 까지 포함한 메서드입니다.

 

 

 

반응형

 

 

4. 비블록 코드 만들기

이제 위 예제를 좀 더 확장하여 여러 가게에서 가격정보를 가져와 노출하는 경우를 추가하겠습니다.

 

코드는 아래와 같습니다.

 

public class ShopTest {

    private static List<Shop> shopList = Arrays.asList(
            new Shop("BestPrice"),
            new Shop("LetsSaveBig"),
            new Shop("MyFavoriteShp["),
            new Shop("BuyItAll")
    );

    public static void main(String[] args) {
        long start = System.nanoTime();
        System.out.println(findPrices("myPhone27S"));
        long duration = (System.nanoTime() - start) / 1000000;
        System.out.println("Done in " + duration + "msecs");
    }

    public static List<String> findPrices(String product) {
        return shopList.stream()
                .map(shop -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product)))
                .collect(Collectors.toList());
    }
}

 

위 예제는 각 shop 마다 delay가 있어 최소 4초 이상의 시간이 걸립니다.

 

 

1) 병렬 스트림으로 요청 병렬화 하기

 

 

위 예제를 앞장에서 배웠던 병렬 스트림을 통해서 성능을 개선시킬 수 있습니다.

 

코드는 아래와 같습니다.

 

public static List<String> findPrices(String product) {
    return shopList.parallelStream()
        .map(shop -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product)))
        .collect(Collectors.toList());
}

 

위와 같이 병렬로 변경 시, 작업은 대략 1/4로 줄어듭니다.

 

2) CompletableFuture로 비동기 호출 구현하기

 

위 병렬 스트림으로 성능이 개선되었지만 비동기를 입혀 블로킹까지 없애 성능을 더욱 올리는게 바람직합니다.

병렬스트림으로 처리하더라도, 위와 같은 경우 한 쓰레드가 shop을 2개 이상 가지게 된다면 블로킹은 여전히 존재합니다.

 

코드는 아래와 같습니다.

 

public static List<String> findPrices(String product) {
    List<CompletableFuture<String>> priceFutures = shopList.stream()
            .map(shop -> CompletableFuture.supplyAsync(
                    () -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product))
            ))
            .collect(Collectors.toList());
    return priceFutures.stream().map(CompletableFuture::join)
            .collect(Collectors.toList());
}

 

 

3) 커스텀 Executor 사용하기

 

위 병렬스트림과 CompletableFuture 를 적용한 성능은 내부적으로 Runtime.getRuntime().availableProcessors() 가 반환하는 스레드 수로 동작합니다.

 

따라서, 쓰레드 풀을 직접 생성하여 동작시킨다면 CompletableFuture를 이용한 비동기 프로그래밍은 더욱 유연해지고 성능 향상이 일어나게 됩니다.

 

아래는 예제입니다.

 

public static List<String> findPrices(String product) {
    List<CompletableFuture<String>> priceFutures = shopList.stream()
            .map(shop -> CompletableFuture.supplyAsync(
                    () -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product))
			, executor
            ))
            .collect(Collectors.toList());
    return priceFutures.stream().map(CompletableFuture::join)
            .collect(Collectors.toList());
}

 

간단히 비동기 처리에 쓸 Executor를 두번째 인자로 추가만 해주면 됩니다.

 

5. 비동기 작업 파이프라인 만들기

위 예제에서 할인율을 더해 출력하는 요구사항을 추가해보도록 하겠습니다.

 

아래는 getPrice 메서드를 shop 이름, 가격, DisCount 정보를 가진 문자열로 반환하도록 변경한 예제입니다.

 

public class Discount {
    
    public enum Code {
        NONE(0),
        SILVER(5),
        GOLD(10),
        PLATINUM(15),
        DIAMOND(20)
        ;
        
        private final int percentage;

        Code(int percentage) {
            this.percentage = percentage;
        }
    }
}

 

public String getPrice(String product) {
    double price = calculatePrice(product);
    Discount.Code code = Discount.Code.values()[random.nextInt(Discount.Code.values().length)];
    return String.format("%s:%.2f:%s", name, price, code);
}

 

1) 할인 서비스 구현

 

위 getPrice의 문자열을 파싱하여 사용하는 클래스는 아래 Quote로 정의하였습니다.

추가로, 이제 Quote 객체를 Discount에 넘겨 할인이 적용된 값을 반환하는 메서드를 추가했습니다.

 

public class Quote {

    private final String shopName;
    private final double price;
    private final Discount.Code code;

    public Quote(String shopName, double price, Discount.Code code) {
        this.shopName = shopName;
        this.price = price;
        this.code = code;
    }

    public static Quote parse(String s) {
        String[] split = s.split(":");
        
        String shopName = split[0];
        double price = Double.parseDouble(split[1]);
        Discount.Code code = Discount.Code.valueOf(split[2]);
        return new Quote(shopName, price, code);
    }

    public String getShopName() {
        return shopName;
    }

    public double getPrice() {
        return price;
    }

    public Discount.Code getCode() {
        return code;
    }
}

 

public class Discount {

    public enum Code {
        NONE(0),
        SILVER(5),
        GOLD(10),
        PLATINUM(15),
        DIAMOND(20)
        ;

        private final int percentage;

        Code(int percentage) {
            this.percentage = percentage;
        }
    }
    
    public static String applyDiscount(Quote quote) {
        return quote.getShopName() + "price is " + 
                Discount.apply(quote.getPrice(), quote.getCode());
    }
    
    private static double apply(double price, Code code) {
        delay();
        return price * (100 - code.percentage) / 100;
    } 
}

 

2) 할인 서비스 사용

 

이제 위 추가된 내용을 사용하기에 가장 쉬운 방법은 아래와 같이 stream으로 처리하는 것입니다.

 

public static List<String> findPrices(String product) {
    return shopList.stream()
        .map(shop -> shop.getPrice(product))
        .map(Quote::parse)
        .map(Discount::applyDiscount)
        .collect(Collectors.toList());
}

 

위 코드는 처리만 할 뿐 성능 최적화와는 거리가 멀게 됩니다.

 

 

2) 동기 작업과 비동기 작업 조합하기

 

위 코드를 비동기를 이용한 코드로 바꿔보겠습니다.

 

예제는 아래와 같습니다.

public static List<String> findPrices(String product) {
    List<CompletableFuture<String>> priceFutures = shopList.stream()
            .map(shop -> CompletableFuture.supplyAsync(
                    () -> shop.getPrice(product), executor)
            )
            .map(future -> future.thenApply(Quote::parse))
            .map(future -> future.thenCompose(
                    quote -> CompletableFuture.supplyAsync(
                            () -> Discount.applyDiscount(quote), executor
                    )
                )
            )
            .collect(Collectors.toList());
    
    return priceFutures.stream().map(CompletableFuture::join)
            .collect(Collectors.toList());
}

 

위 코드에서 thenApply 는 CompletableFuture가 끝날때까지 블록하지 않습니다.

 

따라서 블로킹 없이 CompletableFuture<String> 에서 CompletableFuture<Quote>로 변환됩니다.

 

추가로, thenCompose 메서드는 첫번째 연산의 결과를 두번째 연산으로 전달하는 메서드입니다.

위에서는 quote의 결과를 받으면 Discount.applyDiscount 메서드를 한번 더 비동기로 수행시키는 코드입니다.

 

 

3) 독립 CompletableFuture 와 비독립 CompletableFuture 합치기

 

개발을 하다보면 두개의 독립적인 비동기 작업을 합쳐야 하는 경우가 있습니다.

이런 경우 thenCombine 메서드를 활용하시면 됩니다.

 

예제는 아래와 같습니다.

 

Future<Double> futurePriceInUSD = 
        CompletableFuture.supplyAsync(() -> shop.getPrice(prduct))
        .thenCombine(CompletableFuture.supplyAsync(() -> exchangeService.getRate(Money.EUR, Money.USD)), (price, rate) -> price * rate);

 

아래는 두 Future를 합치는 작업을 도식화 한것입니다.

 

 

출처 : 모던 자바 인 액션

 

 

4) 타임아웃 효과적으로 사용하기

 

Future의 단점으로는 계산이 길어지는 경우 무한정 대기할 수도 있다는 부분입니다.

 

CompletableFuture에서는 이를 위해 orTimeout 메서드를 제공합니다.

 

아래는 예제입니다.

 

Future<Double> futurePriceInUSD = 
        CompletableFuture.supplyAsync(() -> shop.getPrice(prduct))
        .thenCombine(CompletableFuture.supplyAsync(() -> exchangeService.getRate(Money.EUR, Money.USD)), (price, rate) -> price * rate)
        .orTimeout(3, TimeUnit.SECONDS);

 

 

6. CompletableFuture의 종료에 대응하는 방법

CompletableFuture는 thenAccept라는 메서드를 제공합니다.

 

이 메서드는 연산 결과를 소비하는 Consumer를 인수로 받아 사용합니다.

 

아래는 예제입니다.

 

CompletableFuture[] futures = shopList.stream()
    .map(shop -> CompletableFuture.supplyAsync(
            () -> shop.getPrice(product), executor)
    )
    .map(future -> future.thenApply(Quote::parse))
    .map(future -> future.thenCompose(
            quote -> CompletableFuture.supplyAsync(
                    () -> Discount.applyDiscount(quote), executor
            )
        )
    )
    .map(f -> f.thenAccept(System.out::println))
    .toArray(size -> new CompletableFuture[size]);

 

7. 마무리

이번 포스팅에서는 Chapter16  CompletableFuture : 안정적 비동기 프로그래밍 대해 진행하였습니다.

다음에는 Chapter17 리액티브 프로그래밍에 대해 포스팅하겠습니다.

반응형
반응형

1. 서론

 

이번 포스팅에서는 Chapter15의 CompletableFuture와 리액티브 프로그래밍의 컨셉의 기초에 대해 진행하도록 하겠습니다.

 

2. 동시성을 구현하는 자바 지원의 진화

초기 자바는 Runnable과 Thread를 동기화된 클래스와 메서드를 이용해 잠갔습니다.

 

그 후, 자바 5에서는 ExecutorService 인터페이스를 제공하며 스레드 실행과 태스크를 분리하였고,

값을 반환하는 Callable도 ExecutorService로 사용할 수 있게 되었습니다.

ExecutorService는 Runnable, Callable 둘 다 인자로 받을 수 있습니다.

 

자바 8에서는 Future의 진화 단계인 CompletableFuture를 제공, 자바 9에서는 발행-구독 메커니즘을 위한 Flow를 제공하게 되었습니다.

 

CompletableFuture, Flow 의 제공 목표는 가능한한 동시에 블록킹 되지 않게 실행할 수 있도록 제공하기 위함입니다.

 

 

1) Executor와 쓰레드 풀

 

쓰레드의 문제

 

쓰레드는 자바 뿐만이 아닌 하드웨어, 운영체제에서도 최대 사용가능한 갯수가 있습니다.

 

만약 자바 어플리케이션의 쓰레드가 운영체제가 지원하는 쓰레드 수를 초과하면 에러가 발생 할 수 있습니다.

 

쓰레드 풀 그리고 쓰레드 풀이 더 좋은 이유

 

쓰레드 풀은 쓰레드를 계속 생성하는 것이 아니라 사용이 끝나면 반환하게 하여 재사용을 할 수 있도록 하는 방법입니다.

 

장점은 위의 쓰레드 문제같이 에러 나는 경우를 예방할 수 있으며, 쓰레드를 재사용할 수 있다는 점입니다.

 

쓰레드 풀 그리고 쓰레드 풀이 나쁜 이유

 

쓰레드 풀이 장점만 있는 것은 아닙니다.

 

쓰레드 풀을 사용 시 유휴상태의 쓰레드가 풀에 없다면 태스크는 블록킹되기 때문입니다.

 

3. 동기 API와 비동기 API

동기 API는 쓰레드가 CPU 자원을 점유한 상태로 아무런 일을 하지 않는 상황을 만듭니다.

 

이러한 동기 API의 문제점을 아래 예제 코드를 통해 보겠습니다.

 

public class ThreadExample {

    public static void main(String[] args) throws InterruptedException {
        
        int x = 1337;

        Result result = new Result();
        
        Thread t1 = new Thread(() -> {result.left = f(x);});
        Thread t2 = new Thread(() -> {result.right = g(x);});
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        System.out.println(result.left + result.right);
    }
    
    private static class Result {
        private int left;
        private int right;
    }
}

 

위 예제는 f, g 라는 연산을 수행 후 두 결과를 합쳐 출력하는 예제입니다.

 

쓰레드를 2개 만들어 사용하지만 join 메서드로 인해 이들은 동시성이라고 할 수 없습니다.

 

 

1) Future 형식 API

 

위 예제를 Future 를 사용한다면 조금 개선이 될 수 있습니다.

 

Future는 태스크의 작업을 비동기로 쓰레드에서 수행하도록 합니다.

 

아래는 Future를 적용한 예제입니다.

 

int x = 1337;
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<Integer> y = executorService.submit(() -> f(x));
Future<Integer> z = executorService.submit(() -> g(x));

System.out.println(y.get()  + z.get());

 

2) 리액티브 형식 API

 

위 문제에서 Future 를 적용하였더라도 get() 메서드는 블로킹 되기 때문에 완벽한 해결책은 아닙니다.

 

리액티브 형식으로는 처리가 완료 되었을때의 콜백함수를 인자로 넣어 사용하는 것입니다.

 

아래는 예제입니다.

 

int x = 1337;

Result result = new Result();

f(x, (int y) -> {
    result.left = y;
    System.out.println((result.left + result.right));
});

g(x, (int z) -> {
    result.right = z;
    System.out.println((result.left + result.right));
});

 

 

위 같이 수행한다면 블록킹 되는 부분은 없을 겁니다.

하지만, f 와 g의 합계가 정확하게 출력되지 않으며 2번이나 출력이 될 것입니다.

 

이런 경우, 일반적으로 if-then-else 를 이용하여 해결 할 수 있습니다.

 

 

3) 잠자기(그리고 기타 블로캉 동작)는 해로운 것으로 간주

 

블로킹 코드가 있다면 쓰레드는 자원을 점유한 채 아무일도 하지 않을 것입니다.

 

이러한, 작업들이 조금씩 쌓이게되면 프로그램 전체에 영향이 가게 됩니다.

 

그러므로, 블로킹 동작은 최대한 배제해야하며 해로운 것으로 간주해야 합니다.

 

 

4) 비동기 API에서 예외는 어떻게 처리하는가?

 

비동기 API에서 호출된 메서드는 별도의 쓰레드에서 수행되기 때문에 호출자에서 예외를 핸들링 할 수 없습니다.

 

때문에, CompletableFuture에서는 런타임 get() 메서드에 예외를 처리할 수 있는 기능을 제공하였고,

exceptionally() 로 예외에서 회복할 수 있는 메서드도 제공하였습니다.

 

리액티브 형식에서는 값을 콜백형식으로 처리하기 때문에 예외 또한 콜백으로 처리하게됩니다.

 

플로를 예로 든다면 아래와 같이 예외에 대한 콜백 함수가 있습니다.

 

void onError(Throwable throwable)

 

 

 

 

 

 

 

반응형

 

 

 

 

 

 

 

4. CompletableFuture와 콤비네이터를 이용한 동시성

CompletableFuture는 complete 메서드를 통해 나중에 어떤 값을 이용해 다른 쓰레드가 이를 완료할 수 있게 허용합니다.

 

아래는 위 예제를  CompletableFuture를 이용한 예제로 바꾼 코드입니다.

 

ExecutorService executorService = Executors.newFixedThreadPool(2);
int x = 1337;

CompletableFuture<Integer> a = new CompletableFuture<>();
executorService.submit(() -> a.complete(f(x)));
int b = g(x);
System.out.println(a.get() + b);

 

위 예제의 경우 CompletableFuture를 사용하기만 했지 Future로 했을때처럼 블로킹은 존재합니다.

 

CompletableFuture에서는 이러한 문제를 해결하기 위해 thenCombine 메서드를 제공합니다.

 

아래는 예제입니다.

 

ExecutorService executorService = Executors.newFixedThreadPool(10);
int x = 1337;

CompletableFuture<Integer> a = new CompletableFuture<>();
CompletableFuture<Integer> b = new CompletableFuture<>();
CompletableFuture<Integer> c = a.thenCombine(b, (y, z) -> y + z);
executorService.submit(() -> a.complete(f(x)));
executorService.submit(() -> b.complete(g(x)));

System.out.println(c.get());

 

c의 경우 a와 b의 결과가 반환되기 전까지 쓰레드로 수행되지 않습니다.

 

때문에, 이제 블로킹이 없는 코드가 완성되었습니다.

 

5. 발행-구독 그리고 리액티브 프로그래밍

Future는 한번만 실행하여 결과를 제공합니다.

반면, 리액티브 프로그래밍은 시간이 흐르면서 여러 Future 같은 객체를 통해 여러 결과를 제공합니다.

 

자바 9에서는 java.util.concurrent.Flow 인터페이스에 발행-구독 모델을 적용하여 리액티브 프로그래밍을 제공합니다.

 

플로 API는 아래와 같이 3가지로 정리할 수 있습니다.

 

  1. 구독자가 구독할 수 있는 발행자
  2. 이 연결을 구독이라 합니다.
  3. 이 연결을 이용해 메시지 또는 이벤트를 전송합니다.

 

1) 발행 구독 예제

 

아래는 발행-구독의 간단한 예제 입니다.

 

public class SimpleCell implements Publisher<Integer>, Subscriber<Integer> {
    
    private int value = 0;
    private String name;
    private List<Subscriber> subscriberList = new ArrayList<>();

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

    @Override
    public void subscriber(Subscriber<? super Integer> subscriber) {
        subscriberList.add(subscriber);
    }
    
    private void notifyAllSubscribers() {
        subscriberList.forEach(subscriber -> subscriber.onNext(this.value));
    }

    @Override
    public void onNext(Integer newValue) {
        this.value = newValue;
        System.out.println(this.name + ":" + this.value);
        notifyAllSubscribers();
    }
}

 

public class FlowTest {

    public static void main(String[] args) {
        SimpleCell c3 = new SimpleCell("C3");
        SimpleCell c2 = new SimpleCell("C2");
        SimpleCell c1 = new SimpleCell("C1");

        c1.subscriber(c3);

        c1.onNext(10);
        c1.onNext(20);
    }
}

 

위 예제의 결과는 아래와 같이 출력됩니다.

 

C1 : 10
C3 : 10
C2 : 20

 

2) 역압력

 

발행-구독 모델에서는 구독자가 처리할 수 있는 양에 비해 발행자가 무수히 많은 데이터를 전달할 수 있으며, 이러한 상황을 압력이라고 합니다.

 

압력 현상이 발생 시 구독자는 처리할 양만 늘어나며 나중에는 많은 작업으로 어떤 문제가 발생할 수 있을지 모릅니다.

때문에, 구독자가 처리할 수 있을때만 발행자에게 데이터를 전달받도록 조절하는 것이 필요하며, 이를 역압력이라고 합니다.

 

플로 API에서는 Subscription을 통해 이 역압력을 지원합니다.

 

아래는 Subscription 입니다.

 

public static interface Subscription {
    public void request(long n);
    public void cancel();
}

 

Subscription 은 발행자와 구독자의 중간에서 소통을 해주는 역할로 이해하시면 됩니다.

 

아래는 Publisher, Subscriber, Subscription에 대해 그림으로 나타낸 것입니다.

 

 

6. 마무리

 

이번 포스팅에서는 Chapter15 CompletableFuture와 리액티브 프로그래밍의 컨셉의 기초에 대해 진행하였습니다.

다음에는 Chapter16 CompletableFuture : 안정적 비동기 프로그래밍에  대해 포스팅하겠습니다.

반응형
반응형

1. 서론

이번 포스팅에서는 Chapter14의 자바 모듈 시스템에 대해 진행하도록 하겠습니다.

 

2. 압력 : 소프트웨어 유추

일반적으로 자바 언어는 유지보수가 쉬운 언어라고 합니다.

 

그 이유는 관심사 분리, 정보 은닉의 특징을 제공하기 때문입니다.

 

1) 관심사 분리

 

관심사 분리란 프로그램을 고유의 기능으로 나누는 동작을 권장하는 원칙입니다.

 

관심사 분리의 장점은 아래와 같습니다.

 

  1. 개별 기능을 따로 작업할 수 있으므로 팀이 쉽게 협업할 수 있습니다.
  2. 개별 부분을 재사용하기 쉽습니다.
  3. 전체 시스템을 쉽게 유지보수할 수 있습니다.

이러한 관심사 분리를 도울도록 자바 8에서는 모듈 시스템을 제공하였습니다.

 

2) 정보 은닉

 

정보 은닉은 어떤 부분을 변경하였을 때, 다른 부분까지 영향을 미칠 가능성을 줄일 수 있게 합니다.

 

3. 자바 모듈 시스템을 설계한 이유

 

1) 모듈화의 한계

 

자바 9 이전에는 모듈화된 소프트웨어 프로젝트를 만드는데 한계가 있었습니다.

 

자바는 클래스, 패키지, jar 세 가지 수준의 코드 그룹화를 제공합니다.

하지만 패키지와 jar 수준에서 캡슐화를 거의 지원하지 못했습니다.

 

제한된 가시성 제어

 

자바는 클래스 단위에서는 private, protected, public 으로 가시성을 제공합니다.

하지만 패키지 단위에서는 제공하고 있지 않는것이 문제가 되었습니다.

클래스는 다르고, 같은 패키지에서만 공개하여 사용하고 싶은 경우
다른 패키지에서도 사용이 가능한 public으로 해야하기 때문입니다.

 

클래스 경로

 

자바에는 태생적으로 클래스 경로와 JAR 조합에 약점을 가지고 있습니다.

 

  1. 클래스 경로에는 같은 클래스를 구분하는 버전 개념이 없습니다.
  2. 클래스 경로는 명시적인 의존성을 지원하지 않습니다.
    1. 한 JAR가 다른 JAR에 포함된 클래스 집합을 사용하라는 명시적인 의존성을 지원하지 않는다는 의미입니다.

 

 

반응형

 

 

 

4. 자바 모듈 큰 그림

자바 8에서는 모듈이라는 새로운 구조 단위를 제공합니다.

 

모듈 디스크립터는 module-info.java 라는 특별한 파일에 저장되고, 보통 패키지와 같은 폴더에 위치합니다.

 

아래는 자바 모듈 디스크립터의 구조를 도식화 한것입니다.

 

 

5. 여러 모듈 활용하기

 

1) exports 구문

 

exports는 패키지 단위로 다른 모듈에서도 사용할 수 있도록 공개하는 키워드입니다.

 

아래는 한 모듈의 exports 예제입니다.

 

module expenses.readers {
    exports com.example.expenses.readers;
    exports com.example.expenses.readers.file;
    exports com.example.expenses.readers.http;
}

 

2) requires 구문

 

requires는 의존하고 있는 모듈을 지정하는 키워드입니다.

 

기본적으로 모든 모듈은 java.base 라는 모듈을 의존하고 있습니다.

때문에, 이 java.base는 생략가능하며, java.base 가 아닌 모듈을 의존하는 경우에 requires를 사용하면 됩니다.

 

3) 이름 정하기

 

모듈명의 경우 자바 8에서 새롭게 나온 개념으로 이름을 정하는 것에 대한 정확한 규칙이 없습니다.

다만, 오라클에서는 패키지명처럼 인터넷 도메인명을 역순으로 모듈의 이름을 정하도록 추천하고 있습니다.

 

6. 모듈 정의와 구문들

모듈 정의는 module-info.java 파일에 해야하며, 이 파일은 src/main/java 디렉터리에 있어야 합니다.

 

정의할때 사용하는 구문들은 위에서 살펴본 requires, exports 말고도,  requires-transitive, exports-to, open, opens, uses, provides 구문들이 있습니다.

 

1)  requires-transitive

 

이 구문은 다른 모듈이 제공하는 공개 형식을 한 모듈에서 사용할 수 있다고 지정할 수 있습니다.

 

아래는 예제입니다.

 

module com.iteratrlearning.ui {
    requires transitive com.iteratrlearning.core;

    exports com.iteratrlearning.ui.panels;
    exports com.iteratrlearning.ui.widgets;
}

module com.iteratrlearning.application {
    requires com.iteratrlearning.ui
}

 

결과적으로 application 모듈은 core에서 제공하는 공개 형식에 접근할 수 있습니다.

 

 

2) exports to

 

exports to 구문은 사용자에게 공개할 기능을 제한함으로 가시성을 좀 더 정교하게 제어할 수 있습니다.

 

아래는 예제 입니다.

 

module com.iteratrlearning.ui {
    requires com.iteratrlearning.core;

    exports com.iteratrlearning.ui.panels;
    exports com.iteratrlearning.ui.widgets to
            com.iteratrlearning.ui.widgetuser;
}

 

위 예제는 com.iteratrlearning.ui.widgets 에 접근 권한을 가진 사용지의 권한을 com.iteratrlearning.ui.widgetuser로 제한합니다.

 

 

3) open 과 opens

 

모듈 선언에 open 키워드를 사용하면 모든 패키지를 다른 모듈에 반사적으로 접근을 허용할 수 있습니다.

 

전체 모듈을 개발하지 않고도 opens 구문을 모듈 선언에 이용해 필요한 개별 패키지만 개방할 수 있습니다.

 

7. 마무리

 

이번 포스팅에서는 Chapter14 자바 모듈 시스템에 대해 진행하였습니다.

다음에는 Chapter15 CompletableFuture와 리액티브 프로그래밍의 컨셉의 기초에  대해 포스팅하겠습니다.

반응형

+ Recent posts