Try using it in your preferred language.

English

  • English
  • 汉语
  • Español
  • Bahasa Indonesia
  • Português
  • Русский
  • 日本語
  • 한국어
  • Deutsch
  • Français
  • Italiano
  • Türkçe
  • Tiếng Việt
  • ไทย
  • Polski
  • Nederlands
  • हिन्दी
  • Magyar
translation

AI가 번역한 다른 언어 보기

제이온

[Spring] @Async 사용 방법

  • 작성 언어: 한국어
  • 기준국가: 모든 국가 country-flag

언어 선택

  • 한국어
  • English
  • 汉语
  • Español
  • Bahasa Indonesia
  • Português
  • Русский
  • 日本語
  • Deutsch
  • Français
  • Italiano
  • Türkçe
  • Tiếng Việt
  • ไทย
  • Polski
  • Nederlands
  • हिन्दी
  • Magyar

durumis AI가 요약한 글

  • Java에서 비동기 처리를 구현하는 방법으로 @Async 어노테이션을 사용하는 방법을 설명하고 있으며, 특히 Spring에서 @Async를 사용할 때 스레드 풀을 설정하는 방법을 자세하게 다루고 있다.
  • 또한, @Async를 사용할 때 리턴 타입에 따른 반환 형태를 Future, ListenableFuture, CompletableFuture 등으로 나누어 설명하고, 각 타입의 특징과 장단점을 비교 분석한다.
  • 마지막으로 @Async의 장점과 주의 사항을 소개하며, private 메소드에 @Async를 붙일 경우 AOP가 동작하지 않는다는 점과 같은 객체 내의 메소드끼리 호출할 시에도 AOP가 동작하지 않는다는 점을 강조한다.

Java 비동기 처리

Spring @Async를 살펴 보기 전에 동기, 비동기, 멀티 스레드의 개념은 필수적이다. 해당 개념을 알고 있다고 가정하고, 순수 Java 비동기 처리 방식을 코드로 알아 보자. 만약 Java 스레드의 익숙하다면 해당 챕터는 건너뛰어도 무방하다.


public class MessageService {

    public void print(String message) {
        System.out.println(message);
    }
}

public class Main {

    public static void main(String[] args) {
        MessageService messageService = new MessageService();

        for (int i = 1; i <= 100; i++) {
            messageService.print(i + "");
        }
    }
}


만약 message를 받아서 단순히 출력하는 기능을 동기 방식으로 만든다면 위와 같이 코드를 작성할 수 있다. 이를 멀티 스레딩 비동기 방식으로 바꾸면 다음과 같이 소스 코드를 작성할 수 있다.


public class MessageService {

    public void print(String message) {
        new Thread(() -> System.out.println(message))
                .start();
    }
}

public class Main {

    public static void main(String[] args) {
        MessageService messageService = new MessageService();

        for (int i = 1; i <= 100; i++) {
            messageService.print(i + "");
        }
    }
}


하지만 이 방법은 Thread를 관리할 수 없어서 매우 위험하다. 가령, 동시에 10,000개의 호출이 이뤄진다면 아주 짧은 시간에 Thread를 10,000개 생성해야 한다. Thread를 생성하는 비용은 적지 않기 때문에 프로그램의 성능에 악영향을 미치며, 심지어는 OOM 에러가 발생할 수 있다. 따라서 Thread를 관리하기 위해서는 Thread Pool을 구현해야 하고, Java에서는 ExecutorService 클래스를 제공하고 있다.


public class MessageService {

    private final ExecutorService executorService = Executors.newFixedThreadPool(10);

    public void print(String message) {
        executorService.submit(() -> System.out.println(message));
    }
}

public class Main {

    public static void main(String[] args) {
        MessageService messageService = new MessageService();

        for (int i = 1; i <= 100; i++) {
            messageService.print(i + "");
        }
    }
}


전체 스레드의 개수를 10개로 제한하고 있고, 우리가 원하는 멀티 스레딩 방식의 비동기 처리도 올바르게 할 수 있게 되었다. 하지만, 위 방식은 비동기 방식으로 처리하고 싶은 메소드마다 ExecutorService의 submit() 메소드를 적용해야 하므로 반복적인 수정 작업을 해야 한다. 즉, 처음에는 동기 로직으로 작성한 메소드를 비동기로 바꾸고 싶다면 메소드 자체의 로직 변경이 불가피하다는 것이다.


Spring @Async

단순한 방법

@EnableAsync
@SpringBootApplication
public class DemoApplication {

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

}

@Service
public class MessageService {

    @Async
    public void print(String message) {
        System.out.println(message);
    }
}

@RequiredArgsConstructor
@RestController
public class MessageController {

    private final MessageService messageService;

    @GetMapping("/messages")
    @ResponseStatus(code = HttpStatus.OK)
    public void printMessage() {
        for (int i = 1; i <= 100; i++) {
            messageService.print(i + "");
        }
    }
}


@EnableAsync 어노테이션을 Application 클래스 위에 붙여 주고, 비동기 방식으로 처리하고 싶은 동기 로직의 메소드 위에 @Async 어노테이션을 붙이면 끝이다. 하지만 위 방식은 스레드를 관리하지 않는다는 문제가 있다. 왜냐하면 @Async의 기본 설정은 SimpleAsyncTaskExecutor를 사용하도록 되어 있는데, 이것은 스레드 풀이 아니고 단순히 스레드를 만들어내는 역할을 하기 때문이다.


스레드 풀을 사용하는 방법

우선 Application 클래스에서 @EnableAsync를 제거한다. Application 클래스에 @EnableAutoConfiguration 혹은 @SpringBootApplication 설정이 되어 있다면, 런타임 시 @Configuration이 설정된 SpringAsyncConfig 클래스(아래에서 생성할 예정)의 threadPoolTaskExecutor 빈 정보를 읽어 들이기 때문이다.


@Configuration
@EnableAsync // Application이 아닌, Async 설정 클래스에 붙여야 함.
public class SpringAsyncConfig {

    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(3); // 기본 스레드 수
        taskExecutor.setMaxPoolSize(30); // 최대 스레드 수
        taskExecutor.setQueueCapacity(100); // Queue 사이즈
        taskExecutor.setThreadNamePrefix("Executor-");
        return taskExecutor;
    }

}


core와 max 사이즈를 설정할 수 있다. 이때 최초 core 사이즈만큼 동작하다가 이 이상 작업을 처리할 수 없을 경우 max 사이즈만큼 스레드가 증가할 것이라고 예상하겠지만 그렇지 않다.


내부적으로 Integer.MAX_VALUE 사이즈의 LinkedBlockingQueue를 생성해서 core 사이즈만큼의 스레드에서 작업을 처리할 수 없을 경우 Queue에서 대기하게 된다. Queue가 꽉 차게 되면 그때 max 사이즈만큼 스레드를 생성해서 처리하게 된다.


이때 Queue 사이즈를 Integer.MAX_VALUE로 설정하기 부담스럽다면 queueCapacity를 설정해 줄 수 있다. 위와 같이 설정한다면, 최초 3개의 스레드에서 처리하다가 처리 속도가 밀릴 경우 작업을 100개 사이즈 Queue에 넣어 놓고, 그 이상의 요청이 들어오면 최대 30개의 스레드를 생성해서 작업을 처리하게 된다.


스레드 풀 설정이 완료되었다면, @Async 어노테이션이 붙은 메소드에서 위 빈의 이름을 붙이면 된다.


@Service
public class MessageService {

    @Async("threadPoolTaskExecutor")
    public void print(String message) {
        System.out.println(message);
    }
}


만약에 스레드 풀의 종류를 여러 개 설정하고 싶다면, threadPoolTaskExecutor() 같은 빈 생성 메소드를 여러 개 만들고, @Async 설정할 때 원하는 스레드 풀 빈을 넣으면 된다.


리턴 타입 별 반환되는 형태

리턴 값이 없는 경우

비동기로 처리해야 하는 메소드가 처리 결과를 전달할 필요가 없는 경우이다. 이 경우에는 @Async 어노테이션의 리턴 타입을 void로 설정하면 된다.


리턴 값이 있는 경우

Future, ListenableFuture, CompletableFuture 타입을 리턴 타입으로 사용할 수 있다. 비동기 메소드의 반환 형태를 new AsyncResult() 로 묶으면 된다.


[Future]

@Service
public class MessageService {

    @Async
    public Future print(String message) throws InterruptedException {
        System.out.println("Task Start - " + message);
        Thread.sleep(3000);
        return new AsyncResult<>("jayon-" + message);
    }
}

@RequiredArgsConstructor
@RestController
public class MessageController {

    private final MessageService messageService;

    @GetMapping("/messages")
    @ResponseStatus(code = HttpStatus.OK)
    public void printMessage() throws ExecutionException, InterruptedException {
        for (int i = 1; i <= 5; i++) {
            Future future = messageService.print(i + "");
            System.out.println(future.get());
        }
    }
}

// 실행 결과
Task Start - 1
jayon-1
Task Start - 2
jayon-2
Task Start - 3
jayon-3
Task Start - 4
jayon-4
Task Start - 5
jayon-5


future.get() 은 블로킹을 통해 요청 결과가 올 때까지 기다리는 역할을 한다. 그래서 비동기 블로킹 방식이 되어버려 성능이 좋지 않다. 보통 Future는 사용하지 않는다.


[ListenableFuture]

@Service
public class MessageService {

    @Async
    public ListenableFuture print(String message) throws InterruptedException {
        System.out.println("Task Start - " + message);
        Thread.sleep(3000);
        return new AsyncResult<>("jayon-" + message);
    }
}

@RequiredArgsConstructor
@RestController
public class MessageController {

    private final MessageService messageService;

    @GetMapping("/messages")
    @ResponseStatus(code = HttpStatus.OK)
    public void printMessage() throws InterruptedException {
        for (int i = 1; i <= 5; i++) {
            ListenableFuture listenableFuture = messageService.print(i + "");
            listenableFuture.addCallback(System.out::println, error -> System.out.println(error.getMessage()));
        }
    }
}

// 실행 결과
Task Start - 1
Task Start - 3
Task Start - 2
jayon-1
jayon-2
Task Start - 5
jayon-3
Task Start - 4
jayon-4
jayon-5


ListenableFuture은 콜백을 통해 논블로킹 방식으로 작업을 처리할 수 있다. addCallback() 메소드의 첫 번째 파라미터는 작업 완료 콜백 메소드, 두 번째 파라미터는 작업 실패 콜백 메소드를 정의하면 된다. 참고로, 스레드 풀의 core 스레드를 3개로 설정했으므로 “Task Start” 메시지가 처음에 3개 찍히는 것을 확인할 수 있다.



[CompletableFuture]

ListenableFuture만으로도 논블로킹 로직을 구현할 수 있지만, 콜백 안에 콜백이 필요할 경우 콜백 지옥이라고 불리는 매우 복잡한 코드를 유발하게 된다.


Untitled


물론 이번 시간은 CompletableFuture을 자세하게 다루지 않을 것이므로 복잡한 콜백을 대처하는 코드가 궁금하다면 아래 출처의 링크를 참고하길 바란다.


@Service
public class MessageService {

    @Async
    public CompletableFuture print(String message) throws InterruptedException {
        System.out.println("Task Start - " + message);
        Thread.sleep(3000);
        return new AsyncResult<>("jayon-" + message).completable();
    }
}

@RequiredArgsConstructor
@RestController
public class MessageController {

    private final MessageService messageService;

    @GetMapping("/messages")
    @ResponseStatus(code = HttpStatus.OK)
    public void printMessage() throws InterruptedException {
        for (int i = 1; i <= 5; i++) {
            CompletableFuture completableFuture = messageService.print(i + "");
            completableFuture
                    .thenAccept(System.out::println)
                    .exceptionally(error -> {
                        System.out.println(error.getMessage());
                        return null;
                    });
        }
    }
}


ListenableFuture의 콜백 정의보다 가독성이 좋아졌으며, 논블로킹 기능을 완벽하게 수행한다. 따라서 @Async를 사용할 때 리턴 값이 필요하다면 CompletableFuture를 사용할 것을 권장한다.


@Async의 장점

개발자는 메소드를 동기 방식으로 작성하다가, 비동기 방식을 원한다면 단순히 @Async 어노테이션을 메소드 위에 붙여 주면 된다. 그래서 동기, 비동기에 대해 유지 보수가 좋은 코드를 만들 수 있다.


@Async의 주의 사항

@Async 기능을 사용하기 위해서는 @EnableAsync 어노테이션을 선언하는데, 이때 별도의 설정을 하지 않으면 프록시 모드로 동작한다. 즉, @Async 어노테이션으로 동작하는 비동기 메소드는 모두 스프링 AOP의 제약 사항을 그대로 따르게 된다. 자세한 이유는 해당 포스팅을 참고하길 바란다.


  • private 메소드에 @Async를 붙여도 AOP가 동작하지 않는다.
  • 같은 객체 내의 메소드끼리 호출할 시 AOP가 동작하지 않는다.


출처

제이온
제이온
제이온
제이온
[Java] Synchronized Collection vs Concurrent Collection 자바에서 동기화된 컬렉션(Vector, Hashtable, Collections.synchronizedXXX)은 멀티 스레드 환경에서 동시성을 보장하지만, 성능 저하와 여러 연산을 묶어 사용할 때 문제 발생 가능성이 있습니다. 대안으로 java.util.concurrent 패키지의 병렬 컬렉션(CopyOnWriteArrayList, ConcurrentHashMap 등)을 사용하면 읽기 성능 향상과 효율적인 동시성 처리가 가능합니다.

2024년 4월 25일

[Spring] Filter, Interceptor, Argument Resolver란? 필터는 웹 컨테이너에서 동작하며, 디스패처 서블릿에 요청이 전달되기 전/후에 추가 작업을 처리할 수 있는 기능을 제공합니다. 주로 요청 파라미터 검증 및 처리, 보안 관련 공통 작업, 로깅, 이미지/데이터 압축, 문자열 인코딩 등에 사용됩니다.

2024년 4월 27일

[이펙티브 자바] 아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라 정적 팩터리 메서드는 생성자 대신 인스턴스를 생성하는 데 사용할 수 있는 유용한 방법입니다. 이름을 가질 수 있고, 생성자보다 더 많은 유연성을 제공하며, 플라이웨이트 패턴, 싱글톤 패턴, 서비스 제공자 프레임워크와 같은 디자인 패턴을 구현하는 데 사용할 수 있습니다.

2024년 4월 27일

Rust가 동시성 버그를 방지하는 방법 Rust는 강력한 타입 시스템을 통해 동시성 프로그래밍에서 발생하는 일반적인 버그를 컴파일 타임에 감지하여 안전성을 높입니다. 특히, 스레드에 값을 전달할 때 move 클로저를 사용하여 값을 이동시켜야 하고, 여러 스레드에서 공유되는 변수는 Arc와 Mutex와 같은 내부 가변성 패턴을 활용하여 안전하게 관리할 수 있습니다.
곽경직
곽경직
곽경직
곽경직
곽경직

2024년 3월 28일

[Concurrency] Atomic Operation: Memory Fence와 Memory Ordering Atomic 연산에서 메모리 순서를 고려하는 것은 동시성 처리에 필수적입니다. CPU 최적화로 인해 명령어 순서가 바뀌는 현상이 발생할 수 있으며, 이는 동시성 환경에서 문제를 일으킬 수 있습니다. Memory Fence와 Ordering 옵션을 통해 이러한 문제를 해결할 수 있습니다. Ordering 옵션에는 Relaxed, Acquire, Release, AcqRel, SecCst가 있으며, 각각 다른 수준의 메모리 순서 보장을 제공합니다.
곽경직
곽경직
곽경직
곽경직
곽경직

2024년 4월 12일

[비전공, 개발자로 살아남기] 14. 신입 개발자 자주 묻는 기술면접 내용 요약 신입 개발자 면접에서 자주 나오는 기술 질문과 답변을 정리했습니다. 메모리 영역, 자료구조, 데이터베이스, 프로그래밍 패러다임, 페이지 교체 알고리즘, 프로세스와 스레드, OSI 7 계층, TCP와 UDP 등 다양한 주제를 다룹니다.
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자

2024년 4월 3일

한국투자증권 API 개발 시행착오에 대한 기록 한국투자증권 API를 활용해 자동 매매 프로그램을 개발하는 과정에서 겪었던 어려움과 해결 과정을 담은 블로그 글입니다. 계좌 개설, 모의투자 미지원, 웹소켓, 매매 방법론 등 다양한 문제에 대한 경험과 해결 방안을 공유하고 있습니다. 특히 웹소켓 문제 해결을 위해 다른 개발자의 리파지토리를 참고한 경험을 상세히 설명하며, 개발 과정에서 겪는 어려움을 솔직하게 드러냅니다.
(로또 사는 아빠) 살림 하는 엄마
(로또 사는 아빠) 살림 하는 엄마
(로또 사는 아빠) 살림 하는 엄마
(로또 사는 아빠) 살림 하는 엄마
(로또 사는 아빠) 살림 하는 엄마

2024년 4월 23일

자동 매매 프로그램 개선 아이디어 그리드 매매법 자동화 프로그램에 추가하면 좋을 기능들을 정리했습니다. 빅 이벤트 발생 시 홀딩 기능, 투자금 관리 로직 개선, 숏 포지션 적용, 매수/매도 금액 직접 입력 기능 등이 포함됩니다.
(로또 사는 아빠) 살림 하는 엄마
(로또 사는 아빠) 살림 하는 엄마
(로또 사는 아빠) 살림 하는 엄마
(로또 사는 아빠) 살림 하는 엄마
(로또 사는 아빠) 살림 하는 엄마

2024년 4월 21일

Supabase, 그리고 FCM을 사용하여 실시간 푸시알림 시스템 구축하기 Deno, Supabase, Firebase Cloud Messaging(FCM)을 사용하여 실시간 푸시 알림 시스템을 구축하는 방법에 대한 안내입니다. Deno와 클라우드 서비스에 관심 있는 개발자에게 유용한 정보를 제공합니다.
Kofsitho
Kofsitho
Deno, Supabase, 그리고 Firebase Cloud Messaging을 사용하여 실시간 푸시 알림 시스템 구축하기
Kofsitho
Kofsitho

2024년 2월 8일