웹사이트 검색

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


스프링 스레드와 메서드 호출자는 메서드 실행이 완료될 때까지 기다리지 않습니다. 이 예제에서는 자체 서비스를 정의하고 Spring Boot 2를 사용합니다. 시작하자!

스프링 @Async 예제

Maven을 사용하여 데모용 샘플 프로젝트를 만들 것입니다. 프로젝트를 생성하려면 작업 공간으로 사용할 디렉터리에서 다음 명령을 실행합니다.

mvn archetype:generate -DgroupId=com.journaldev.asynchmethods -DartifactId=JD-SpringBoot-AsyncMethods -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
<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을 이해하기 위해 일부 종속성을 추가할 때 프로젝트에 대한 전체 종속성 트리를 볼 수 있는 간단한 Maven 명령을 실행할 수 있습니다. 사용할 수 있는 명령은 다음과 같습니다.

mvn dependency:tree

비동기 지원 활성화

비동기 지원을 활성화하는 것도 단일 주석의 문제입니다. 비동기 실행을 활성화하는 것 외에도 스레드 제한을 정의할 수 있는 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 {
    ...
}

여기서 우리는 백그라운드 스레드 풀에서 비동기 메서드를 실행할 수 있는 Spring의 기능을 활성화하는 @EnableAsync 주석을 사용했습니다. 다음으로 언급된 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;

    // standard getters and setters

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

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

서비스 만들기

언급된 Movie 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);
        // Artificial delay of 1s for demonstration purposes
        Thread.sleep(1000L);
        return CompletableFuture.completedFuture(results);
    }
}

이 클래스는 Spring Component Scan에 적합하도록 만드는 @Service입니다. lookForMovie 메서드의 반환 유형은 모든 비동기 서비스의 요구 사항인 CompletableFuture입니다. API의 타이밍이 다를 수 있으므로 데모를 위해 2초의 지연을 추가했습니다.

명령줄 실행기 만들기

애플리케이션을 테스트하는 가장 쉬운 방법인 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 {
        // Start the clock
        long start = System.currentTimeMillis();

        // Kick of multiple, asynchronous lookups
        CompletableFuture<MovieModel> page1 = movieService.lookForMovie("58611129-2dbc-4a81-a72f-77ddfc1b1b49");
        CompletableFuture<MovieModel> page2 = movieService.lookForMovie("2baf70d1-42bb-4437-b551-e5fed5a87abe");
        CompletableFuture<MovieModel> page3 = movieService.lookForMovie("4e236f34-b981-41c3-8c65-f8c9000b94e7");

        // Join all threads so that we can wait until all are done
        CompletableFuture.allOf(page1, page2, page3).join();

        // Print results, including elapsed time
        LOG.info("Elapsed time: " + (System.currentTimeMillis() - start));
        LOG.info("--> " + page1.get());
        LOG.info("--> " + page2.get());
        LOG.info("--> " + page3.get());
    }
}

방금 RestTemaplate를 사용하여 임의로 선택한 동영상 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의 Asynchronous 기능을 사용하는 방법을 학습했습니다. 여기에서 Spring 관련 게시물을 더 읽어보세요.

소스 코드 다운로드

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