여러분은 모처럼 스케이트를 타기 위해 아이스링크를 찾았습니다. 그런데 스케이트가 없군요! 이때 여러분에게는 두 가지 선택지가 있습니다. 스케이트 만드는 장인에게 스케이트를 주문해서 구입하는 방법과 스케이트 대여소에서 스케이트를 빌리는 방법입니다.
우리가 앞에서 배운 스레드를 생성해서 사용하는 방식이 바로 스케이트 장인에게 스케이트를 주문 제작하여 사용하는 방법이라고 볼 수 있습니다.
그리고 이제부터 배울 스레드 풀 은 대여소로부터 스케이트를 빌리는 방법이라고 생각하시면 됩니다.
Thread Pool 은 사용자들의 요청에 스레드를 빌려주고, 다 쓴 스레드를 반납받는 구조를 가지고 있습니다.
스레드 풀을 만드는 방법은 두 가지가 있습니다. 바로 newCachedThreadPool과 newFixedThreadPool인데요. 이름에 cache와 fixed 가 들어간 만큼 빌려줄 스레드를 관리하는데 차이가 있습니다.
Cache는 빨리 꺼내주기 위한 임시 저장을 의미합니다. 그래서 newCachedThreadPool 은 스레드 요청이 있으면 우선 만들어 빌려 주고, 이후 반납받은 스레드를 저장해 두었다가 빌려줍니다.
반면 fixed는 고정을 의미하죠? newFixedThreadPool 은 처음부터 지정한 n 개의 스레드를 유지하고 있다가 요청이 있으면 빌려 줍니다.
여러분들이 스케이트 대여소의 사장님이라고 했을 때 어떻게 하는 것이 더 효율적이고, 어떤 상황에 더 유리할지 생각해 봅시다.
우리가 스케이트 장에서 스케이트를 빌릴 때는 어떤 목적으로 탈 것인지를 이야기하고 알맞은 스케이트를 빌립니다. 스레드 풀에게도 우리가 어떤 작업을 할 것인지 작업 내용을 작성해야 합니다.
이때 우리가 원하는 작업의 결과가 반환이 있는 것인지 없는 것인지에 따라 작성 형식은 달라지게 됩니다.
반환 값이 있을 때는 Callable 인터페이스를 구현받아 그 형식에 맞게 작성해야 하고, 반환 값이 없다면 Runnable 인터페이스를 구현받아 형식에 맞게 작성해야 합니다.
한 가지 팁을 드리자면 우리가 누군가를 부르면(Call) 그 사람이 대답을 하죠? 그래서 Call 이 들어간 인터페이스로 작업을 요청하면 반환 값이 있어야 한다라고 생각하면 구분이 쉽습니다.
그리고 이렇게 해야 할 일을 적어 스레드 풀에 요청합니다. 이때 요청 하는 방식은 총 두 가지가 있습니다. 바로 execute()와 submit() 메서드입니다.
Submit 은 정식 제출을 의미합니다. 그리고 제출한 내용에 대한 결괏값을 받게 되어 있습니다. 그래서 반환 값을 받아야 하는 Callable 인터페이스 형태가 들어가게 됩니다.
Execute는 그냥 실행을 의미합니다. 그리고 결괏값이 없습니다. 그래서 반환하는 값이 없는 Runnable 인터페이스 형태가 들어가게 됩니다. 이 외에도 둘은 다른 특징이 있지만 우선 이 정도만 알아 두기로 하겠습니다.
당연히 스레드 풀에서 빌려온 녀석들도 스레드 이기 때문에 순서 없이 움직이는 건 마찬가지입니다.
그래서 이들을 제어할 방법이 필요한데요, 스레드 풀에서도 특정 스레드가 끝날 때까지 기다리게 하는 join과 같은 blocking 기능이 있습니다.
바로 Callable을 이용한 방식입니다.
Callable 은 무조건 반환 값이 있을 때만 쓰인다고 했지요? Callable 객체를 이용해 submit() 메서드를 실행하면 Future라는 객체 형태를 반환하게 되는데요, 이때 get() 메서드를 사용하면 실제로 실행한 반환 값이 나오게 됩니다.
그리고 자동으로 이 값이 나오기 전까지 다른 스레드들은 먼저 실행되지 못하도록 막고 있게 됩니다.
그럼 반환 값이 없는 작업을 하면 blocking 기능을 사용하지 못하나요?
작업 후 반환 값이 없을 경우는 Runnable 인터페이스 형태로 작업을 한다고 했습니다.
그리고 Runnable 은 execute() 메서드를 사용하기에 blocking 기능을 사용할 수 없습니다.
그래서 어쩔 수 없이 반환값이 없는 작업인 Runnable 작업이라 해도 submit()을 이용할 수 있도록 해주었습니다.
그래서 반환하는 값은 아무것도 없지만 get() 메서드를 사용하여 blocking 기능을 사용할 수 있습니다.
스케이트 장도 영업이 끝나는 시간이 있죠?
스레드 풀도 언제까지 운영할 수는 없으니 모든 스레드를 회수하고 문을 닫아야 하는 경우가 있습니다.
이때 문을 닫는 방법은 총 두 가지입니다.
첫 번째는 shutdownNow() 메서드를 통해 닫는 방법입니다.
이 방법은 thread의 stop() 메서드처럼 아무 준비 없이 바로 닫아버리기 때문에 활동 중이던 스레드들은 아무런 대비 없이 그대로 종료되게 됩니다. 문제 발생이 예상돼죠? 그래서 별로 추천하지 않는 방법입니다.
두 번째는 shutdown() 메서드를 통해 닫는 방법입니다.
이 방법은 대여되었던 모든 스레드가 반납될 때까지 기다렸다가 닫는 방법입니다. 가장 이상적인 방법입니다만 나가지 않는 진상 손님이 발생하면 끝끝내 닫지 못하는 불상사가 생기게 됩니다.
그래서 awaitTermination()이라는 메서드를 사용해 최대 기다리는 시간을 지정하는 것을 권장합니다.
awaitTermination() 은 특정 시간 동안 기다렸다가 스레드 풀을 닫고, 진상손님이 있어서 강제로 닫았는지 여부를 반환해 줍니다.
이렇게 말이죠!
이렇게 스레드 풀까지 모두 배워 보았습니다. 스레드 풀에서는 의도치 않게 많은 코드가 나왔던 것 같습니다. 하지만 이 코드를 꼭 이해할 필요는 없습니다. 우리가 여기서 배우려는 목적 자체는 자바의 주요 개념과 쓰임새에 대해서 알아보는 것 이니까요.