● java.lang.Thread를 상속하여 작성하는 방법
Java에서 스레드를 사용하는 방법은 다른 언어에 비해 쉬운 편입니다. java.lang.Thread를 사용한 클래스를 만들거나, java.lang.Runnable 인터페이스를 구현한 클래스를 만들어 스레드를 시작(start)하기만 하면 됩니다.
public class OpenmaruThread extends Thread {
public void run() {
System.out.println("Hello OPENMARU!");
}
public static void main(String args[]) {
OpenmaruThread openmaru = new OpenmaruThread();
openmaru.start();
}
}
● java.lang.Runnable 인터페이스를 구현하여 작성하는 방법
public class OpenmaruThread extends Runnable {
public void run() {
System.out.println("Hello OPENMARU!");
}
public static void main(String args[]) {
Thread openmaru = new Thread(new OpenmaruThread());
openmaru.start();
}
}
같은 일을 반복하여 수행하는 스레드를 생성하고, 소멸하는 데 시간이 오래 걸리기 때문에 Pool을 사용하는 것이 훨씬 효율적인 방법이 될 것입니다.
Thread Pool을 사용하려면 작업 요청을 관리하는 작업 큐 관리, 동시성 문제, 스레드의 재활용 문제 등 고려할 것이 많아 좀 복잡해집니다. Java 언어가 발전하면서 java.concurrent API에서 이런 Thread Pool을 사용하는 간단하고 다양한 방법을 제공하고 있습니다.
● Executor를 이용하는 방법
다음은 스레드 풀을 만들고, Runnable 인터페이스를 구현한 작업을 실행하는 아주 간단한 예제입니다.
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(() -> System.out.println("Hello OPENMARU"));
● Executor Service를 이용하는 방법
다음은 Executor Service를 이용하여, 2개의 스레드를 관리할 수 있는 스레드 풀을 만들고 실행 결과를 얻어오는 예제입니다. 간단한 API를 이용하여 스레드 풀을 손쉽게 구현할 수 있습니다.
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future future = executorService.submit(() -> "Hello OPENMARU");
System.out.println("result=" + future.get());
이외에도 정해진 시간 간격으로 실행하는 ScheduledExecutorService같은 서비스를 사용할 수 있습니다.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
executor.scheduleAtFixedRate(() -> System.out.println("Hello OPENMARU"), 500, 1000, TimeUnit.MILLISECONDS);
● ForkJoinPool을 이용하는 방법
Java 7에서 소개된 ForkJoinPool은 Divide and Conquer방법을 통해 병렬 처리를 구현할 수 있는 API도 제공하고 있습니다.
OPENMARU APM에서 비동기 스레드 추적방법
사용자 로그인을 처리하는 애플리케이션이 3개의 다른 서비스를 호출하여야 하는 경우가 있을 것입니다. 그런데, 타 서비스 3개 최소 응답시간이 모두 3초라고 생각하면, 로그인하는데 최소 9초 이상의 시간이 소요될 것입니다. 이 시간을 줄이는 방법은 3개의 서비스 호출을 모두 비동기 스레드로 호출하고 그 결과를 모아서 로그인을 처리하게 되면 3초 정도 시간에 로그인 처리를 완료할 수 있을 것입니다.
그런데, 3개 중 어떤 하나의 서비스가 느려서 그 원인을 분석하려고 할 때, 일반적인 Java APM(Application Performance Monitoring) 솔루션에서 별도의 스레드에서 실행되는 내용을 분석할 수 없었습니다.
왜냐하면, Java 애플리케이션을 모니터링하기 위한 모든 APM(Application Performance Monitoring/Management) 솔루션들은 메소드 단위의 모니터링 데이터를 수집하여 저장하기 위하여 ThreadLocal을 사용합니다. ThreadLocal이란 스레드 단위로 변수를 할당하여 사용하는 기능입니다. 같은 스레드에서 실행되는 값들을 스레드 단위로 저장하기 때문에 스레드에서 실행되는 Java API들의 Hooking 하여 추적값들을 저장하는데 사용합니다.
그런데, 문제는 스레드 단위로 변수를 저장하기 때문에 다른 스레드를 생성하여 처리하는 비동기 스레드일 경우에 저장할 수 없습니다. Java 1.2부터 InheritableThreadLocal이란 클래스를 제공하여 자식 스레드에 그 변숫값을 넘겨줄 수 있지만, 객체를 생성할 때만 넘겨줄 수 있기 때문에 스레드 풀이나 Cache를 사용할 경우에는 다른 값이 들어 있는 문제가 발생합니다.
OPENMARU APM에서는 최근 5.1.0- 8.4 버전부터 Java의 비동기 스레드를 모니터링하는 기능을 추가하여 제공하고 있습니다. 물론 스레드 풀, 위의 예제에서 설명한 Runnable 인터페이스를 구현하여 ExecutorService를 사용하는 경우, ForkJoinPool, CompletableFuture를 사용하는 경우에도 가능합니다.
● OPENMARU APM의 ForkJoinPool 모니터링 방법
트랜잭션 히트맵(T-Map)의 상세 트랜잭션 조회 화면에서 스레드를 사용한 경우에는 ‘Relationship’ 버튼이 표시되고, 버튼을 클릭하면 스레드 간의 호출관계가 표시되어 추적이 가능합니다. 어떤 스레드가 무슨 작업을 실행했고 어디서 느렸는지 원인을 분석할 수 있습니다.
● OPENMARU APM의 Runnable 모니터링 방법
ForkJoin은 다른 스레드를 실행한 결과를 기다리고 호출한 스레드가 종료하지만, 호출한 스레드가 먼저 끝나버리는 경우에도 모니터링이 가능할까요?
아래 예제는 메인 스레드가 10개의 자식 스레드를 호출하고 먼저 끝나 버려서 19ms 걸렸지만, 자식 스레드들은 2~5초 정도 수행된 상황입니다. Relationship 버튼을 클릭하면 아래와 같이 호출한 어떤 스레드가 어디서 느렸는지 파악할 수 있습니다.
마치며
동기, 비동기와 Blocking, Non-Blocking의 차이점에 대해서 살펴보았으며, Java의 비동기 스레드를 처리하는 몇가지 Java API에 대해서 알아보았습니다. 또, OPENMARU APM에서 Java 비동기 스레드가 느릴 때 어떤 구간이 느린지 확인할 수 있는 방법을 살펴보았습니다.
OPENMARU APM에서는 비동기 스레드의 모니터링을 기반으로, webflux, gRPC등 비동기로 구현된 Framework 들어 대한 지원을 추가해 나갈 것입니다.
글쓴이 : 오픈마루 연구소
2023년 9월 충청 지역 클라우드 네이티브 세미나 자료 다운로드
/in Cloud, OPENMARU, Seminar, 발표자료/by 주하 원2023년 8월 충청권 공공기관에서 진행된 찾아가는 클라우드 네이티브 세미나
/in Cloud, Seminar, 발표자료/by 주하 원찾아가는 클라우드 네이티브 세미나 – 인천 소재 공공기관
/in Cloud, Seminar, 발표자료, 오픈나루 공지사항/by 주하 원