비동기 처리를 위한 Spring @Async 주석

@Async 주석을 사용하면 스프링에서 비동기 메서드를 만들 수 있습니다. 이 튜토리얼에서 스프링 프레임워크에서 @Async를 탐험해 봅시다. 간단히 말해서 빈의 메서드에 @Async 주석을 달면 스프링은 해당 메서드를 별도의 스레드에서 실행하고 메서드 호출자는 메서드의 실행이 완료될 때까지 기다리지 않습니다. 이 예에서는 자체 서비스를 정의하고 Spring Boot 2를 사용합니다. 시작해 봅시다!

@Async 예제

이 예제에서는 Maven을 사용하여 데모용 샘플 프로젝트를 만들 것입니다. 프로젝트를 만들려면 작업 공간으로 사용할 디렉토리에서 다음 명령을 실행하십시오:

mvn archetype:generate -DgroupId=com.journaldev.asynchmethods -DartifactId=JD-SpringBoot-AsyncMethods -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

만약 처음으로 메이븐을 실행하는 경우 메이븐은 생성 작업을 수행하기 위해 필요한 모든 플러그인 및 아티팩트를 다운로드해야 하기 때문에 몇 초가 걸릴 것입니다. 여기 프로젝트 생성이 어떻게 보이는지에 대한 내용이 있습니다: 프로젝트를 생성한 후에는 즐겨 사용하는 IDE에서 열어도 괜찮습니다. 다음 단계는 프로젝트에 적절한 메이븐 종속성을 추가하는 것입니다. 여기 적절한 종속성이 포함된 pom.xml 파일이 있습니다:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

마지막으로 이 종속성을 추가했을 때 프로젝트에 추가된 모든 JAR를 이해하기 위해 간단한 메이븐 명령을 실행할 수 있습니다. 이 명령을 사용하면 프로젝트에 종속성을 추가할 때 완전한 종속성 트리를 볼 수 있습니다. 사용할 수 있는 명령은 다음과 같습니다:

mvn dependency:tree

이 명령을 실행하면 다음과 같은 종속성 트리가 표시됩니다:

비동기 지원 활성화

Async 지원을 활성화하는 것은 하나의 주석만으로 가능합니다. Async 실행을 활성화하는 것 외에도 Executor를 사용하여 스레드 제한을 정의할 수도 있습니다. 코드를 작성하면 더 자세히 알아보겠습니다.

package com.journaldev.asynchexample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@SpringBootApplication
@EnableAsync
public class AsyncApp {
    ...
}

여기에서는 @EnableAsync 주석을 사용하여 Spring의 비동기 메서드를 백그라운드 스레드 풀에서 실행할 수 있도록 합니다. 다음으로, 언급한 Executor도 추가합니다.

@Bean
public Executor asyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(2);
    executor.setMaxPoolSize(2);
    executor.setQueueCapacity(500);
    executor.setThreadNamePrefix("JDAsync-");
    executor.initialize();
    return executor;
}

여기에서 최대 2개의 스레드가 동시에 실행되고 대기열 크기는 500으로 설정되도록 합니다. 완성된 클래스의 전체 코드는 다음과 같습니다.

package com.journaldev.asynchexample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@SpringBootApplication
@EnableAsync
public class AsyncApp {

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

    @Bean
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("JDAsync-");
        executor.initialize();
        return executor;
    }
}

다음으로 실제로 스레드 실행을 수행하는 서비스를 만들겠습니다.

모델 만들기

영화 데이터를 반환하는 공개 영화 API를 사용할 것입니다. 이를 위해 모델을 정의합니다.

package com.journaldev.asynchexample;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class MovieModel {

    private String title;
    private String producer;

    // 표준 getter와 setter

    @Override
    public String toString() {
        return String.format("MovieModel{title='%s', producer='%s'}", title, producer);
    }
}

Spring이 응답에서 더 많은 속성이 있는 경우에도 안전하게 무시할 수 있도록 @JsonIgnoreProperties를 사용했습니다.

서비스 만들기

지금은 언급된 영화 API를 호출하는 우리의 서비스를 정의할 때입니다. 우리는 간단한 RestTemplate을 사용하여 GET API를 호출하고 비동기적으로 결과를 얻을 것입니다. 사용하는 샘플 코드를 살펴보겠습니다:

package com.journaldev.asynchexample;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.concurrent.CompletableFuture;

@Service
public class MovieService {

    private static final Logger LOG = LoggerFactory.getLogger(MovieService.class);

    private final RestTemplate restTemplate;

    public MovieService(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder.build();
    }

    @Async
    public CompletableFuture lookForMovie(String movieId) throws InterruptedException {
        LOG.info("Looking up Movie ID: {}", movieId);
        String url = String.format("https://ghibliapi.herokuapp.com/films/%s", movieId);
        MovieModel results = restTemplate.getForObject(url, MovieModel.class);
        // 시연을 위한 1초의 인위적인 지연
        Thread.sleep(1000L);
        return CompletableFuture.completedFuture(results);
    }
}

이 클래스는 @Service로 표시되어 있으며 Spring Component Scan에 적합합니다. lookForMovie 메서드의 반환 유형은 CompletableFuture로, 이것은 비동기 서비스에 대한 요구 사항입니다. API의 타이밍이 다를 수 있으므로, 시연을 위해 2초의 지연을 추가했습니다.

Command Line Runner 만들기

우리는 CommandLineRunner를 사용하여 애플리케이션을 테스트하는 가장 간단한 방법으로 앱을 실행할 것입니다. CommandLineRunner는 애플리케이션의 모든 빈이 초기화된 직후에 실행됩니다. CommandLineRunner에 대한 코드를 살펴보겠습니다:

package com.journaldev.asynchexample;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.concurrent.CompletableFuture;

@Component
public class ApplicationRunner implements CommandLineRunner {

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationRunner.class);

    private final MovieService movieService;

    public ApplicationRunner(MovieService movieService) {
        this.movieService = movieService;
    }


    @Override
    public void run(String... args) throws Exception {
        // 시계 시작
        long start = System.currentTimeMillis();

        // 여러 비동기 조회 시작
        CompletableFuture page1 = movieService.lookForMovie("58611129-2dbc-4a81-a72f-77ddfc1b1b49");
        CompletableFuture page2 = movieService.lookForMovie("2baf70d1-42bb-4437-b551-e5fed5a87abe");
        CompletableFuture page3 = movieService.lookForMovie("4e236f34-b981-41c3-8c65-f8c9000b94e7");

        // 모든 스레드를 결합하여 모두 완료될 때까지 기다립니다
        CompletableFuture.allOf(page1, page2, page3).join();

        // 경과 시간을 포함한 결과 출력
        LOG.info("Elapsed time: " + (System.currentTimeMillis() - start));
        LOG.info("--> " + page1.get());
        LOG.info("--> " + page2.get());
        LOG.info("--> " + page3.get());
    }
}

우리는 단순히 RestTemplate을 사용하여 일부 무작위로 선택된 영화 ID와 함께 샘플 API를 호출했습니다. 우리의 애플리케이션을 실행하여 어떤 출력이 표시되는지 확인할 것입니다.

애플리케이션 실행

애플리케이션을 실행하면 다음과 같은 출력이 나타납니다.

2018-04-13  INFO 17868 --- [JDAsync-1] c.j.a.MovieService  : Looking up Movie ID: 58611129-2dbc-4a81-a72f-77ddfc1b1b49
2018-04-13 08:00:09.518  INFO 17868 --- [JDAsync-2] c.j.a.MovieService  : Looking up Movie ID: 2baf70d1-42bb-4437-b551-e5fed5a87abe
2018-04-13 08:00:12.254  INFO 17868 --- [JDAsync-1] c.j.a.MovieService  : Looking up Movie ID: 4e236f34-b981-41c3-8c65-f8c9000b94e7
2018-04-13 08:00:13.565  INFO 17868 --- [main] c.j.a.ApplicationRunner  : Elapsed time: 4056
2018-04-13 08:00:13.565  INFO 17868 --- [main] c.j.a.ApplicationRunner  : --> MovieModel{title='My Neighbor Totoro', producer='Hayao Miyazaki'}
2018-04-13 08:00:13.565  INFO 17868 --- [main] c.j.a.ApplicationRunner  : --> MovieModel{title='Castle in the Sky', producer='Isao Takahata'}
2018-04-13 08:00:13.566  INFO 17868 --- [main] c.j.a.ApplicationRunner  : --> MovieModel{title='Only Yesterday', producer='Toshio Suzuki'}

자세히 살펴보면 앱에서 JDAsync-1JDAsync-2 두 개의 스레드만 실행되었습니다.

결론

이 강의에서는 Spring Boot 2의 Spring의 비동기 기능을 어떻게 사용할 수 있는지에 대해 공부했습니다. 더 많은 Spring 관련 게시물은 여기에서 확인할 수 있습니다.

소스 코드 다운로드

Spring Boot Async 예제 프로젝트 다운로드

Source:
https://www.digitalocean.com/community/tutorials/spring-async-annotation