개발 알다가도 모르겠네요

[Spring] @Async를 이용해 비동기 처리를 해보자 본문

웹/Spring

[Spring] @Async를 이용해 비동기 처리를 해보자

이재빵 2024. 5. 16. 21:28
728x90

기존에는 일기를 작성하면 내부적으로는 '일기 생성 -> GPT로 일기 감정 추출 -> Suno AI로 음악생성 -> 일기 생성 완료' 의 로직으로 돌아가고 있어 클라이언트가 응답을 받는데까지 7~8초가 소요됐다.

따라서 비동기 처리를 활용하면 장기 실행 작업이 메인 스레드를 차단하지 않도록 하여 사용자 응답 시간을 줄이고 시스템 자원을 효율적으로 사용할 수 있도록 구현했다.

 

비동기 처리를 위한 AsyncConfig 클래스

비동기 처리를 위한 설정 클래스를 작성한다. AsyncConfig 클래스는 Spring의 비동기 기능을 활성화하고, ThreadPoolTaskExecutor를 설정하여 비동기 작업을 처리할 때 사용할 스레드 풀을 구성한다.

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10); // 코어 스레드 풀 크기 설정
        executor.setMaxPoolSize(50); // 최대 스레드 풀 크기 설정
        executor.setQueueCapacity(100); // 대기열 크기 설정
        executor.setThreadNamePrefix("Async-"); // 스레드 이름 접두사 설정
        executor.initialize();
        return executor;
    }
}

 

 

DiaryService 클래스

DiaryService 클래스는 일기와 관련된 비즈니스 로직을 처리한다. 일기를 생성하고 비동기로 GPT 프롬프트 생성&음악 생성을 처리한다.

@Service
public class DiaryService {

    private final UserRepository userRepository;
    private final DiaryRepository diaryRepository;
    private final ChatGPTService chatGPTService;
    private final MusicService musicService;

    public DiaryService(UserRepository userRepository, DiaryRepository diaryRepository, ChatGPTService chatGPTService, MusicService musicService) {
        this.userRepository = userRepository;
        this.diaryRepository = diaryRepository;
        this.chatGPTService = chatGPTService;
        this.musicService = musicService;
    }

    @Transactional
    public DiaryCreateResponseDto createDiary(Long userId, DiaryCreateRequestDto diaryCreateRequestDto) {
        try {
            User user = userRepository.findById(userId)
                    .orElseThrow(() -> new UserException("해당하는 사용자를 찾을 수 없습니다."));

            Diary diary = diaryCreateRequestDto.toEntity(diaryCreateRequestDto, user);
            Diary savedDiary = diaryRepository.save(diary);
            return DiaryCreateResponseDto.of(savedDiary);
        } catch (Exception e) {
            throw new DiaryException("Diary 생성 중 오류가 발생했습니다. " + e.getMessage());
        }
    }

    @Async
    public void handleDiaryCreationAsync(Long diaryId) {
        try {
            generatePromptAsync(diaryId);
            generateMusicAsync(diaryId);
        } catch (Exception e) {
            log.error("Diary 추가 작업 중 오류가 발생했습니다. " + e.getMessage());
        }
    }

    @Async
    public void generatePromptAsync(Long diaryId) {
        try {
            Diary diary = diaryRepository.findById(diaryId)
                    .orElseThrow(() -> new DiaryException("Diary not found with id: " + diaryId));

            PromptRequestDto promptRequestDto = new PromptRequestDto(diary.getContent());
            PromptResponseDto promptResponseDto = chatGPTService.sendPrompt(promptRequestDto, PromptType.CONTENT_EMOTION_ANALYSIS);

            String updatedPrompt = diary.getPrompt() + " , " + promptResponseDto.getContent();
            diary.setPrompt(updatedPrompt);

            diaryRepository.save(diary);
        } catch (Exception e) {
            log.error("GPT 프롬프트 처리 중 오류가 발생했습니다. " + e.getMessage());
        }
    }

    @Async
    public void generateMusicAsync(Long diaryId) {
        try {
            Diary diary = diaryRepository.findById(diaryId)
                    .orElseThrow(() -> new DiaryException("Diary not found with id: " + diaryId));

            MusicCreateRequestDto musicCreateRequestDto = new MusicCreateRequestDto();
            musicCreateRequestDto.setDiaryId(diary.getId());
            musicCreateRequestDto.setGenerationPrompt(diary.getPrompt());

            musicService.createMusic(musicCreateRequestDto);
        } catch (Exception e) {
            log.error("음악 생성 중 오류가 발생했습니다. " + e.getMessage());
        }
    }
}

 

기술적 인사이트

  1. 비동기 처리의 장점: 비동기 처리를 통해 여러 작업을 동시에 처리할 수 있으므로, 사용자 요청에 대한 응답 시간을 단축할 수 있었다. 일기 작성 후 감정 분석 및 음악 생성과 같은 시간이 오래 걸리는 작업을 비동기로 처리했기 때문에 UX 측면에서 개선됐다.
  2. ThreadPoolTaskExecutor 설정: 비동기 작업의 효율적인 처리를 위해 ThreadPoolTaskExecutor를 사용하여 스레드 풀을 설정하였다. 코어 스레드 풀 크기, 최대 스레드 풀 크기, 대기열 크기 등을 설정하여 다양한 작업 부하를 처리할 수 있도록 구성했다.
  3. 비동기 메서드 구현: Spring의 @Async 애노테이션을 사용하여 비동기 메서드를 구현하였다. 일기 생성 후, GPT 프롬프트 생성 및 음악 생성을 비동기 메서드로 처리하여 메인 프로세스의 성능을 저하시키지 않도록 했다.
@Async
public void handleDiaryCreationAsync(Long diaryId) {
    try {
        generatePromptAsync(diaryId);
        generateMusicAsync(diaryId);
    } catch (Exception e) {
        log.error("Diary 추가 작업 중 오류가 발생했습니다. " + e.getMessage());
    }
}