티스토리 뷰

 

그동안 서버에서 비동기 처리를 할 일이 많지 않았었는데 이번에 멀티쓰레드를 처음 사용해볼 기회가 생겼다.

 

다행히 스프링 부트는 멀티쓰레드 개발의 초심자인 사람도 편하게 사용할 수 있게끔 다양한 기능을 제공해 주고 있었다.

 

이 내용을 소개해 보려고 한다.

 

비동기(Asynchorous)란?

멀티 쓰레드가 필요한 이유.

 

비동기는 다른 곳에서도 많이 다루기 때문에 간단히 말하고 넘어가려고한다.

 

한줄 요약하면, 앞선 작업이 끝나기를 대기하며 수행(동기적)되는게 아니라 앞선 작업의 종료까지 대기하지 않고, 다음 작업을 바로 수행(비동기)한다는 의미이다.

 

다들 한번쯤 봤을 만한 그림을 첨부한다.

 

@Async

 

이 어노테이션 하나면 비동기적으로 동작하게 코드를 구현할 수 있다.

 

자바에서 Thread를 상속받거나 Runnable 인터페이스를 상속받으며 추가 구현부가 있는 것과 다르게 스프링부트 프로젝트 시작 부분에 추가적인 어노테이션(@EnableAsync)만 붙이면 끝이다.

@SpringBootApplication
@EnableAsync
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(YourApplication.class, args);
    }

}

 

이 어노테이션을 붙이고, 비동기로 실행하고 싶은 매서드로 가서 @Async를 붙여주면 된다.

@Service
public class YourService {

    @Async
    public void asyncMethod() {
        // 비동기적으로 실행되는 로직
    }

    // 다른 메서드들...

}

 

초 간단 사용법의 끝.

 

다만, 주의할 점은 몇 가지 있는데

 

@Async 어노테이션은 쓰레드를 재사용하지 않는다.

 

즉, 매번 새로운 쓰레드를 사용한다는 말이다.

 

쓰레드를 매번 새로 생성하는 작업은 큰 자원을 소모하기 때문에 일반적으로는 쓰레드 풀을 미리 만들어 두고 사용한다.

 

또한, @Async가 사용된 매서드가 종료되는지 잘 확인해야한다.

 

디폴트 타임아웃 설정이 되어 있지 않기 때문에, 커넥션 같은게 계속 유지되는 상태라면 쓰레드가 계속 유지된다.

 

이런 것들을 고려하지 않고 사용한다면 여기까지만 보고 사용하면 된다.

 

여기서부터는 심화과정

이러한 설정들을 다루기 위해서는 늘 그렇듯 @Configuration을 통해 추가 설정을 해야한다.

@Configuration
public class AsyncConfig {

    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10); // 코어 스레드 풀 크기 설정
        executor.setMaxPoolSize(20); // 최대 스레드 풀 크기 설정
        executor.setQueueCapacity(500); // 작업 큐 용량 설정
        executor.setThreadNamePrefix("AsyncThread-"); // 스레드 이름 접두사 설정

        // 작업이 완료된 후 스레드 풀이 종료될 때까지 대기할 시간 설정 (단위: 초)
        executor.setAwaitTerminationSeconds(60);

        executor.initialize();
        return executor;
    }

}

이외에 여러 몇 가지 옵션들이 더 있지만, 대표적인 케이스만 추려서 작성했다.

 

또, @Async로 정의된 매서드는 private으로 만들 수 없다는 제약 조건이 있지만 이 경우는 IDE에서 잘 잡아주기 때문에 큰 문제가 안된다.

 

자가호출 불가능은 어기기 어려운 제약 조건이라 여기까지만 작성하려고 한다.

 

 

 

여기까지만 쓰려다가

 

추가적으로 구글링하다 어떤 글을 보게 됐는데 잘 이해가 가지 않는 상황을 예외라고 설명해놨다.

@RestController
public class TestController {
	@Autowired
	private TestService testService;
	
	@GetMapping("/test2")
	public void test2() {
		for(int i=0;i<10000;i++) {
			testService.innerMethodCall(i);
		}
	}
}
@Service
public class TestService {
	
	@Async
	public void innerMethod(int i) {
		logger.info("async i = " + i);
	}
	
	public void innerMethodCall(int i) {
		innerMethod(i);
	}
}

비동기 상황을 잘 이해하지 못하고 작성된 예시이다.

 

이 상황은 당연히 순차적으로 진행될 가능성이 높은 상황이다. (실제로 돌려보면 동기적으로 동작한다)

 

왜냐? 위 상황은 비동기 상황이 아니다.  

 

하지만, 이 코드를 아래와 같이 강제적으로 비동기 상황을 만들면 비동기적으로 동작할 것이다.

@RestController
public class TestController {
	@Autowired
	private TestService testService;
	
	@GetMapping("/test2")
	public void test2() {
		for(int i=0;i<10000;i++) {
			testService.innerMethodCall();
		}
	}
}

@Service
public class TestService {
	
	@Async
	public void innerMethod() {
        for(int i=0;i<10;i++) {
            logger.info("async i = " + i);
            Thread.sleep(1000);
        }
	}
	
	public void innerMethodCall() {
		innerMethod();
	}
}

위의 예시는 비동기적 상황에 대한 이해 부족과 자가 호출하면 안된다는 의미를 잘못 이해해서 그렇다.

 

자가 호출하면 안된다는 의미는 비동기적으로 동작하는 쓰레드에서 재귀 호출하는 매서드를 사용하지 말라는 의미이다.

 

자가 호출이 사실 재귀를 의미하기 때문에, 아래의 예를 보면 이해하기 쉬울 것이다.

@Service
public class ExampleService {

    @Async
    public void asyncMethod() {
        System.out.println("비동기 메서드 실행");
        recursiveMethod(5);
        System.out.println("비동기 메서드 종료");
    }

    public void recursiveMethod(int count) {
        if (count > 0) {
            System.out.println("재귀 메서드 실행: " + count);
            recursiveMethod(count - 1);
        }
    }
}

위와 같은 예시를 실행하면 @Async 어노테이션이 있음에도 동기적으로 동작하게 된다.

 

또 특이한 예지만 아래와 같이 호출해도 동작하지 않는다.

@Service
public class ExampleService {

    @Async
    public void asyncMethod() {
        
    }

}

public class ExternalService {

    public void exampleMethod() {
        ExampleService exService = new ExampleService();
        exService.someOtherMethod(); // ExampleService asyncMethod 직접 호출
    }

}

그런데 위 같은 상황들은 잘 나오지 않을 것이다.

 

재귀 호출할 일은 사실상 잘 없고, 스프링을 조금이라도 배운 개발자라면 DI를 이용해 서비스를 생성할 것이기 때문이다.

 

마치며

예외는 사실상 잘 일어나지 않을 예시기 때문에, 어지간한 상황에서는 초간단 사용법만 알아도 잘 사용할 수 있을 것이다.

 

하지만 상용 서비스에서 비동기를 구현하게 된다면 꼭 @Configuration으로 executor를 어떻게 설정할 수 있나 확인하고 추가 설정을 해주자.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함