[Ch15] CompletableFuture 와 리액티브 프로그래밍 컨셉의 기초

멀티코어 프로세서가 발전하면서 애플리케이션의 속도는 멀티코어 프로세서를얼마나 잘 활용할 수 있도록 소프트웨어를 개발하는가에 따라 달라진다. 한 개의 큰 Task를 병렬로 실행할 수 있는 개별 하위 태스크로 분리하면 병렬성을 높일 수 있다. 자바7 에서는 fork/join 프레임워크, 자바8 에서는 병럴 스트림으로 스레드에 비해 단순하고 효과적인 병렬 실행을 달성할 수 있다.

이것을 아키텍처로 넓게 본다면 MSA로 확장시킬 수 있다. 하나의 거대한 애플리케이션 대신 작은 서비스로 애플리케이션을 나누는 것이다.

추가로 자바는 Future 인터페이스로 자바8의 CompletableFuture 구현을 통해 간단하고 효율적으로 병렬성과 동시성을 제어한다. (16장에서 본다)

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

처음에 자바는 RunnableThread 를 동기화된 클래스와 메서드를 이용해 잠갔다. 그리고 자바5에서 좀 더 표현력 있는 동시성을 지원하는 ExecutorService 인터페이스를 지원했다. 이런 기능 덕분에 다음 해부터 등장한 멀티코어 CPU에서 쉽게 병렬 프로그래밍을 구현할 수 있게 되었다.

자바7에서는 분할정복 알고리즘의 fork/join 을 구현하는 java.util.concurrent.RecursiveTask 가 추가되었고, 자바8에서는 스트림과 새로 추가된 람다 지원에 기반한 병렬 프로세싱이 추가되었다.

자바는 Future 를 조합하는 기능을 추가하면서 동시성을 강화했고, 자바9에서는 분산 비동기 프로그래밍을 명시적으로 지원한다.

CompletableFuturejava.util.concurrent.Flow 의 궁극적인 목표는 가능한한 동시에 실행할 수 있는 독립적인 태스크를 가능하게 만들면서 멀티코어 또는 여러 기기를 통해 제공되는 병렬성을 쉽게 이용하는 것이다.

스레드와 높은 수준의 추상화

단일 CPU 컴퓨터도 여러 사용자를 지원할 수 있다. 운영체제가 각 사용자에 프로세스 하나를 할당하기 때문이다. 운영체제는 주기적으로 번갈아가며 여러 프로세스에 CPU를 할당함으로 자원을 관리한다.

프로세스는 다시 운영체제에 한개 이상의 스레드 즉, 본인이 가진 프로세스와 같은 주소 공간을 공유하는 프로세스를 요청함으로 태스크를 동시에 또는 협력적으로 실행할 수 있다.

자바8의 병렬 스트림은 스레드를 추상화한 개념이라고 보면 된다.

Executor 와 스레드 풀

자바5의 ExecutorService 개념과 스레드 풀 또한 스레드를 추상화한것이다. 자바 프로그래머가 태스크 제출과 실행을 분리할 수있는 기능을 제공했다.

스레드의 문제 자바 스레드는 직접 운영체제 스레드에 접근한다. 운영체제 스레드를 만들고 종료하려면 비싼 비용을 치러야 하며 운영체제 스레드의 숫자는 제한되어 있는 것이 문제다. (그리고 스레드를 딱히 건들고 싶지 않다.)

스레드 풀 그리고 스레드 풀이 더 좋은 이유 놀고있는 스레드, 종료되지 않은 스레드를 관리해줄 수 있는 것이 ExecutorService의 스레드 풀이다.

ExecutorService.newFixedThreadPool(int nThreads);

위의 메서드는 스레드풀을 만들고, 워커 스레드를 실행하며 태스크의 실행이 종료되면 이들 스레드 풀로 반환한다. 이 방식의 장점은 하드웨어에 맞는 수의 태스크를 유지함과 동시에 수 천개의 태스크를 스레드 풀에 아무 오베허드 없이 제출할 수 있다는 점이다.

스레드 풀이 나쁜 이유

스레드를 직접 하용하는 것보다 스레드 풀을 이용하는 것이 바람직하지만, 두 가지를 주의해야 한다.

  • k 스레드를 가진 스레드 풀은 오직 k만큼의 스레드를 동시에 실행할 수 있다. (블락이 있으면 문제다)
  • main이 반환하기 전에 스레드 풀이 종료되지 않는다면 문제가 있을수 있다.
    • Thread.setDaemon 메서드로 보완할 수 있다.

스레드의 다른 추상화 : 중첩되지 않은 메서드 호출

포크/조인 프레임워크에서는 한 개의 태스크나 스레드가 메서드 호출 안에서 시작되면 그 메서드 호출은 반환되지 않고 작업이 끝나기를 기다렸다. 즉 join 과 fork 가 끝나야 메서드의 호출이 반환된다. (이그나이트 일듯) 이를 엄격한 포크조인이라 하며, 시작된 태스크를 내부 호출이 아니라 외부 호출에서 종료하도록 기다리는 좀 더 여유로운 방식(비동기)도 있다.

이러한 스레드 실행은 데이터 정합성과, 데이터 경쟁문제, 그리고 스레드 풀의 종료에 주의를 기울여야 한다.

스레드에 무엇을 바라는가?

일반적으로 모든 하드웨어의 스레드를 활용해 병렬성의 장점을 극대화하도록 프로그램 구조를 만드는 것이 목표이다.

for루프와 분할정복 알고리즘 을 통해 포크/조인을 구현하는 방법을 살혀봤는데, 16장, 17장에서는 스레드를 조작하는 복잡한 코드를 구현하지 않고 메서드를 호출하는 방법을 살펴본다.

댓글남기기