본문 바로가기
실습/리눅스 서버 + 스프링 부트

[JAVA Spring] Thread에 시간제한(Timtout) 걸기

by 이민우 2024. 5. 17.
728x90
반응형

저번 프로젝트를 진행하던 중 한 가지 문제사항에 직면했다.

 

우선 로직은 아래와 같다.

  • 사용자가 데이터를 업로드한다.
  • 프로그램은 데이터를 받아 큐에 임시저장한 후 사용자에게 업로드 완료 알림을 보낸다.
  • 스케줄러가 동작하며 주기적으로 큐에서 데이터를 빼 스레드에 할당해 비즈니스 로직을 실행시킨다.

 

이 때 문제사항이 생겼다.

사용자가 데이터를 업로드할 때, 업로드된 데이터가 너무 큰 경우이다.

 

너무 큰 데이터를 업로드했을 때 시간이 오래 소요되는 것은 당연한 일이다. 하지만 지나치게 오랜 시간이 소요되면 어찌됐던 문제가 발생할 수 있다.

 

게다가 가장 중요한 문제는 위에서 사용자가 업로드한다는 데이터는 파일(엑셀 혹은 csv)인데, 엑셀의 경우 poi 라이브러리를 이용해 구현했다. 하지만 너무 큰 엑셀 파일이 업로드되면 종종 poi 라이브러리가 멈추는 현상이 발생했고, 이로 인해 스레드가 종료되지 않고 계속 실행되는 현상이 발생했다.

 

이 현상을 해결하기 위해 Thread에 시간 제한을 걸어 너무 많은 시간이 소요된 스레드는 강제 종료하는 방법을 검색해 구현했다. 물론 한 가지 흠이 있는 코드이기는 하지만 (추후 설명) 어찌됐든 방법을 저장해 추후 필요 시마다 사용해볼까 한다.

 

 

Future 라이브러리

결로적으로 말하자면 java.util.concurrent.Future 을 사용해서 해결했다.

 

멀티스레드 환경에서 사용되는 Future은 비동기 작업의 상태와 결과를 관리하고 작업의 완료를 기다릴 수 있는 메커니증를 제공한다. 여기서 주목할 점은 작업의 완료를 기다린다는 점인데, 이를 통해 Timeout을 걸 수 있다는 특징이 있다.

 

우선 아래의 코드가 있다고 가정해보자.

package com.mwlee.future;

import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.springframework.stereotype.Service;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class FutureService {

	private static final int WORK_THREADS=10;
	private static ExecutorService executorService = Executors.newFixedThreadPool(WORK_THREADS);
	
	/**
	 * 테스트용 스레드 실행
	 */
	@PostConstruct
	public void doTest() {
		runThread();
	}
	
	/**
	 * 스레드 실행 함수
	 */
	public void runThread() {
		log.info("@@ {} method Start!", getMinAndSec());
		
		executorService.submit(() -> {
			log.info("@@ {} thread Start!", getMinAndSec());
			businessLogic();
			log.info("@@ {} thread Done!", getMinAndSec());
		});

		log.info("@@ {} method Done!", getMinAndSec());
	}
	
	
	/**
	 * 비즈니스 로직. 어떠한 작업으로 11초가 걸린다고 가정.
	 */
	private void businessLogic() {
		log.info("@@ {} businessLogic Start!", getMinAndSec());
		try {
			// 11초 대기
			Thread.sleep(11*1000);
		} catch (InterruptedException e) {
			log.error("@@ {} businessLogin Interrupted!", getMinAndSec());
		}
		log.info("@@ {} businessLogic Done!", getMinAndSec());
	}
	
	/**
	 * 로그 찍기용 분:초 반환
	 */
	public String getMinAndSec() {
		Date date = new Date();
		StringBuffer str = new StringBuffer();
		str.append(String.format("%02d", date.getMinutes()));
		str.append(":");
		str.append(String.format("%02d", date.getSeconds()));
		
		return str.toString();
	}
}

 

 

그러면 아래와 같은 결과가 출력될 것이다.

 

코드대로 비즈니스 로직이 11초가 소요되어 39초에 시작, 50초에 종료됐다.

 

다만 이 때 비즈니스 로직은 10초 이내로 종료되어야 하며, 이를 넘어설 경우 에러로 간주해야 한다는 요구사항이 존재하면 어떻게 될까?

 

이 때 사용되는 것이 Future이다.

 

사용 방법은 아래와 같다. 아래의 로직을

기존

 

이렇게 바꾸기만 하면 된다. Future로 thread를 만들고 get으로 실행시킨 후 타임아웃 시 cancel로 중지시킨다.

변형

 

그러면 아래와 같이 11초가 걸리는 로직이 전부 실행되지 못하고 10초 뒤 끝나는 모습을 볼 수 있다.

24초에 Timeout 발생

 

그런데 위 부분에서 중요한 포인트가 한 가지 존재한다.

"method Done!" 이라는 문구가 Timeout 뒤에 나왔다. 즉, 논블로킹 형태가 아니라 블로킹 형태로 로직이 진행되고 있다.

이유라면 당연히 중간에 try-catch로 감싼 부분이 있기 때문이다.

 

이 이슈를 해결하기 위한 방법은 간단하지만 비효율적이다. 하지만 이 방법 외의 방법은 생각이 나지 않아 굳이 블로그에 업로드를 해야하나 싶었지만, 그래도 써놓으면 쓸 일이 있을 것 같아 작성했다.

 

그 간단하지만 비효율적인 방법은 스레드 안에 스레드를 넣어 두 개의 스레드로 실행을 시키는 것이다.

방법은 runThread 메소드를 아래와 같이 짜면 된다.

스레드 안에 스레드(Future) 사용

 

그러면 아래와 같이 논블로킹으로 수행이 된다.

 

이에 대한 전체 코드는 아래와 같다.

package com.mwlee.future;

import java.util.Date;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.springframework.stereotype.Service;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class FutureService {

	private static final int WORK_THREADS=10;
	private static ExecutorService executorService = Executors.newFixedThreadPool(WORK_THREADS);
	
	/**
	 * 테스트용 스레드 실행
	 */
	@PostConstruct
	public void doTest() {
		runThread();
	}
	
	/**
	 * 스레드 실행 함수
	 */
	public void runThread() {
		log.info("@@ {} method Start!", getMinAndSec());
		
		executorService.submit(() -> {
			Future future = executorService.submit(() -> {
				log.info("@@ {} thread Start!", getMinAndSec());
				businessLogic();
				log.info("@@ {} thread Done!", getMinAndSec());
			});
				
			// 10초 안에 실행되도록 변경.
			try {
				future.get(10, TimeUnit.SECONDS);
			}
			catch(InterruptedException | ExecutionException | TimeoutException e) { 
				log.error("@@ {} method Timeout!", getMinAndSec());
				future.cancel(true); // true이면 강제종료
			}
		});

		log.info("@@ {} method Done!", getMinAndSec());
	}
	
	
	/**
	 * 비즈니스 로직. 어떠한 작업으로 11초가 걸린다고 가정.
	 */
	private void businessLogic() {
		log.info("@@ {} businessLogic Start!", getMinAndSec());
		try {
			// 11초 대기
			Thread.sleep(11*1000);
		} catch (InterruptedException e) {
			log.error("@@ {} businessLogin Interrupted!", getMinAndSec());
		}
		log.info("@@ {} businessLogic Done!", getMinAndSec());
	}
	
	/**
	 * 로그 찍기용 분:초 반환
	 */
	public String getMinAndSec() {
		Date date = new Date();
		StringBuffer str = new StringBuffer();
		str.append(String.format("%02d", date.getMinutes()));
		str.append(":");
		str.append(String.format("%02d", date.getSeconds()));
		
		return str.toString();
	}
}
728x90
반응형