개발공부/SPRING

[Spring] 비동기 프로그래밍 & ThreadPoolExecutor - 실습

키크니개발자 2023. 1. 9. 21:51

 

안녕하세요!

키크니 개발자 입니다. 🦒

 

비동기 프로그래밍에 대해서 강의를 보고 정리하고자 작성하였습니다. 😁

 

AppConfig

package dev.be.async.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class AppConfig {

    @Bean(name = "defaultTaskExecutor")
    public ThreadPoolTaskExecutor defaultTaskExecutor() {
        // 생성자 및 setter를 사용할 수 있다.
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(200);
        executor.setMaxPoolSize(300);
        executor.setKeepAliveSeconds(10);
        return executor;
    }

    @Bean(name = "messagingTaskExecutor", destroyMethod = "shutdown")  // (1)
    public ThreadPoolTaskExecutor messagingTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(200);
        executor.setMaxPoolSize(300);
        executor.setKeepAliveSeconds(10);
        return executor;
    }

}

AppConfig에 ThreadPool 2개를 정의한다.

(1) : destroyMethod : 스레드풀을 정의했지만, 의도치 않게 스레드풀이 정의가 안될 수도 있어서 디스토리 메소드 사용한다.

해당 속성을 확인해 보면 bean 객체의 스코프가 끝날을 경우(스프링에서는 어플리케이션 컨텍스트가 종료되었을 경우로 생각하면 된다.) class 속성에 선언한 클래스의 shutdown 메서드를 호출하는 의미이다. (종료하라는 의미이다.)

ThreadPoolTaskExecutor를 자세히 알고 싶으면 여기를 눌러주세요!

 

AsyncConfig

package dev.be.async.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

@Configuration
@EnableAsync  // (1)
public class AsyncConfig {
}

(1) : @Async는 비동기적으로 처리할 수 있게 해주는 어노테이션이다. 해당 어노테이션을 사용하려면 @EnableAsync가 달려있는 configuration 클래스가 우선적으로 필요하다.

@Async 어노테이션을 사용하기 위해서는 public method 이어야 한다. (프록시를 사용하기 위해서는 메서드가 public 이어야 한다. (아래 추가 설명))

 

AsyncController

package dev.be.async.controller;

import dev.be.async.service.AsyncService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class AsyncController {

    private final AsyncService asyncService;

    @GetMapping("/1")
    public String asyncCall_1() {
        asyncService.asyncCall_1();
        return "success";
    }

    @GetMapping("/2")
    public String asyncCall_2() {
        asyncService.asyncCall_2();
        return "success";
    }

    @GetMapping("/3")
    public String asyncCall_3() {
        asyncService.asyncCall_3();
        return "success";
    }
}

 

AsyncService

package dev.be.async.service;

import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AsyncService {

    private final EmailService emailService;

    public void asyncCall_1() {  // (1)
        System.out.println("[asyncCall_1] :: " + Thread.currentThread().getName());
        emailService.sendMail();
        emailService.sendMailWithCustomThreadPool();
    }

    public void asyncCall_2() {  // (2)
        System.out.println("[asyncCall_2] :: " + Thread.currentThread().getName());
        EmailService emailService = new EmailService();
        emailService.sendMail();
        emailService.sendMailWithCustomThreadPool();
    }

    public void asyncCall_3() {  // (3)
        System.out.println("[asyncCall_3] :: " + Thread.currentThread().getName());
        sendMail();
    }

    @Async
    public void sendMail() {
        System.out.println("[sendMail] :: " + Thread.currentThread().getName());
    }

}

(1) : 빈 주입을 받아서 사용하는 Async 메일

(2) : 인스턴스 선언 (new 사용)

(3) : 내부 메소드를 async 로 선언

 

EmailService

package dev.be.async.service;

import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class EmailService {

    @Async("defaultTaskExecutor")
    public void sendMail() {
        System.out.println("[sendMail] :: " + Thread.currentThread().getName());
    }

    @Async("messagingTaskExecutor")
    public void sendMailWithCustomThreadPool() {
        System.out.println("[messagingTaskExecutor] :: " + Thread.currentThread().getName());
    }
}

AppConfig에서 정의한 ThreadPool 2개를 사용한다.

async나 main thread 비동기를 동작하는 것을 확인해보는 것은 메소드 스레드 이름을 찍어보는 것이 제일 정확하다.

 

결과값

// 빈 주입을 받아서 사용하는 Async 메일
localhost:8080/1    // 스레드가 모두 다른 것을 확인할 수 있다.

[asyncCall_1] :: http-nio-8080-exec-1
[messagingTaskExecutor] :: messagingTaskExecutor-1
[sendMail] :: defaultTaskExecutor-1

// 인스턴스 선언
localhost:8080/2    // 모두 다른 스레드네임이 찍히길 바랬지만, 같은 스레드로 실행되고 있는 것을 확인할 수 있다.(async 하지 않다.)

[asyncCall_2] :: http-nio-8080-exec-4
[sendMail] :: http-nio-8080-exec-4
[messagingTaskExecutor] :: http-nio-8080-exec-4

// 내부 메소드를 async 로 선언
localhost:8080/3    // 모두 같은 스레드인 것을 확인할 수 있다. 

[asyncCall_3] :: http-nio-8080-exec-6
[sendMail] :: http-nio-8080-exec-6

 

 

결론

스프링에서 비동기를 사용하려면 스프링 프레임워크의 도움이 필요하다.

 

비동기 메소드로 잘 처리하려면 첫번째 케이스를 사용해야 되는데 이는 스프링에서 아래 설명과 같이 동작한다.


첫번째 케이스

@Async를 선언한 메소드들을 갖고 있는 EmailService같은 경우에는 빈으로 등록되어있을 것이고,
그 빈을 가져왔을 때 순수한 빈을 AsyncService에게 반환을 해주는 것이 아니라
EmailService가 Async하게 동작을 해야되기 때문에 한 번 더 프록시 객체로 한 번 더 랩핑을 해준다.
그리고 그 프록시 객체로 리턴해준다.

 

AsyncService는 순수한 EmailService 빈을 받는 것이 아니라 한 번 랩핑된 EmailService를 받게 된다.

예를 들어 EmailService.sendMail()를 실행하면 비동기로 동작할 수 있게 sub Thread 에게 위임한다. 스프링 컨테이너에 등록 된 빈을 사용해야 된다.

첫번째 케이스 동작

 

두번째 케이스
스프링 컨테이너에 등록 된 빈이 아니기 때문에 비동기가 되지 않는다.

 

세번째 케이스
AsyncService는 이미 빈을 가져온 상태이고, 해당 빈 안에 있는 메소드를 다이렉트로 접근하게 되면 스프링 프레임워크의 도움을 받을 수 없다. (해당 빈을 프록시 객체로 랩핑할 수 없다.)

 

두번째, 세번째 케이스 동작

 

Async 프로그래밍을 할 때에는 반드시 빈을 주입받아야 한다.

실제로 Async를 사용하는 이유는 많은 동작을 빠르게 혹은 다양하게 분산하기 위해서 사용한다.
이에 대한 장애는 서비스가 운영하는 도중에 동기적으로 사용되는 것을 확인됐을 때 발견된다.
비동기 프로그래밍을 할 때에는 반드시 스레드이름을 찍어본다.
@Async 사용할 땐 public 으로 사용해야된다. private를 사용하게 되면 컴파일 에러가 발생한다.

 

 

async하게 코드를 작성할 때에는 반드시 테스트 코드를 작성해보거나 혹은 실제로 로그를 찍어서 다른 sub thread가 잘 실행 되는지 확인한다.

 

 

 

 

References

10개 프로젝트로 완성하는 백엔드 웹개발(Java/Spring) : https://fastcampus.co.kr/dev_online_befinal

https://velog.io/@alicia-mkkim/%EB%8F%99%EA%B8%B0-

https://one0.tistory.com/15

https://bepoz-study-diary.tistory.com/399

 

 

 

 

 

반응형