Thread와 비동기
Q. 프로젝트에서 Thread를 직접 만들어서 쓰는 경우와 직접 만들었을 때 어떠한 문제점이 발생할 수 있나요?
프로젝트에서 Thread
를 직접 만드는 경우는 보통 무거운 작업을 병렬로 처리하고 싶을 때 입니다.
예를 들어, 여러 개의 이미지 파일을 동시에 조정하거나, 대용량의 동영상 파일을 변환하는 작업처럼요.
이럴 땐 각각의 작업을 별도 쓰레드에서 돌리면 전체 처리 속도를 높일 수 있습니다.
하지만 직접 Thread
를 생성해서 쓰다 보면 몇 가지 문제가 생깁니다.
첫 번째는 예외 처리가 번거로워집니다. Runnable
로 작업을 넘기면 체크 예외를 던질 수 없기 때문에 try-catch로 다 감싸야 합니다.
두 번째는 결과를 받기가 어렵다는 점입니다. 쓰레드 안에서 처리한 결과를 메인 쓰레드로 넘기려면 공유 객체를 쓰거나 콜백 구조로 복잡하게 짜야 합니다.
그리고 마지막으로 가장 큰 문제는 성능 관리에 있습니다. 작업 수만큼 쓰레드를 마구 생성하다 보면 CPU나 메모리를 과하게 쓰게 되고, 컨텍스트 스위칭 비용도 증가하게 됩니다.
이러한 문제를 해결하기위해 Java는 Java 5부터 쓰레드 풀을 미리 만들어두고 거기에 작업을 던지는 방식인 ExecutorService
기능을 제공합니다. 이러면 쓰레드 재사용도 되고, Future
를 통해 결과나 예외도 쉽게 관리할 수 있어서 훨씬 안정적입니다.
Q. Future는 뭔가요?
Future
는 말 그대로 "미래에 결과가 올 거다"를 표현하는 객체입니다.
ExecutorService
로 작업을 실행할 때 submit()
같은 메서드를 사용하면 Future
객체가 리턴되는데, 이걸 가지고 나중에 결과를 꺼내올 수 있습니다.
예를 들어, 어떤 계산 작업을 백그라운드에서 실행하고 future.get()
으로 결과를 받을 수 있는데, 이 get()
은 작업이 끝날 때까지 현재 쓰레드를 블로킹합니다. 그래서 너무 오래 걸리는 작업이라면 get(5, TimeUnit.SECONDS)
처럼 타임아웃을 설정해서 기다리는 시간도 조절할 수 있고요.
단점은 이렇게 결과를 가져오는 방식이 블로킹 기반이라는 점 입니다. 그래서 복잡한 비동기 처리가 필요할 땐 CompletableFuture
처럼 좀 더 유연한 API를 쓰는 게 좋습니다.
Q. 그럼 Future의 블로킹 한계를 보완하기 위해 어떤 방법을 쓸 수 있을까요?
그 한계를 해결하기 위해 자바 8부터는 CompletableFuture
라는 클래스가 도입되었습니다. 이건 비동기 작업을 블로킹 없이 처리할 수 있도록 도와줍니다.
CompletableFuture
는 작업이 끝난 이후에 실행할 로직을 미리 지정할 수 있는 체이닝 메서드들을 제공합니다. 예를 들어 thenApply()
는 결과값을 받아서 다른 값으로 변환할 수 있고, thenAccept()
는 결과를 소비만 하고, exceptionally()
는 예외가 발생했을 때 대체 로직을 실행할 수 있습니다.
또한 supplyAsync()
같은 메서드를 통해 쓰레드를 따로 지정하지 않아도 ForkJoinPool
에서 자동으로 처리되기 때문에, 개발자가 쓰레드 풀을 일일이 관리할 필요도 없죠.
예를 들어 사용자 정보를 DB에서 비동기로 가져오고, 그 결과를 기반으로 이메일을 보내는 작업이 있다고 할 때, CompletableFuture
로 작업들을 연결해서 처리하면 전체 흐름이 깔끔하고, 성능적으로도 효율적입니다.
Last updated