본문 바로가기

웹 프로그래밍/스프링

최종 프로젝트 회고 (스프링 @Asysc, 비동기 방식)

이제는 알람서버로 분리된 기능이고 최종 프로젝트의 핵심 기능인 알람 기능의 동작 방식에 대해서 포스팅 하겠습니다.

팀원분이 공부하고 작성했던 내용을 바탕으로 최종 프로젝트에서 RabbitMQ를 적용하여 알람서버로 분리하는 과정을 추가해서 정리해 보겠습니다.

 

구독한 이용자에게 공연 정보를 Jakarta Mail로 이메일을 보내게 되는 상황에서 동기방식으로 알람을 보내게 되면 모든 알람을 보낼때까지 프로세스가 기다리기 때문에 그때 동안 다른 요청을 처리하지 못하게돼서 성능저하에 이어지게 됩니다.

그래서 이에 대한 해결방법으로 스프링 비동기 방식을 사용할수 있습니다. 핵심은 모든 알람이 전달될때까지 기다리는게 아니라 각각의 알람들은 쓰레드들이 잡고서 해결하고 공연 업로드는 알람처리와 상관없이 업로드돼야 하는것입니다.

 

구현 방법

1) AsyncConfig 설정해서 쓰레드 풀을 만듦

@Configuration
@EnableAsync // 스프링의 비동기 기능을 활성화
public class AsyncConfig implements AsyncConfigurer {

  @Override
  // 쓰레드 풀 이름
  @Bean(name = "threadPoolTaskExecutor")
  public Executor getAsyncExecutor() {
	// 내 PC의 Processor 개수
    int processors = Runtime.getRuntime().availableProcessors(); 
	// TaskExecutor를 사용하여 비동기 작업을 스케줄링 (ThreadPoolTaskExecutor)
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 
    // 기본적으로 실행 대기 중인 스레드 개수
    executor.setCorePoolSize(processors);
    // 동시에 동작하는 최대 스레드 개수
    executor.setMaxPoolSize(processors * 2); 
    // CorePool의 크기를 넘어서면 큐에 저장하는데, 그 큐의 최대 용량
    executor.setQueueCapacity(50); // 대기를 위한 Queue 크기
    executor.setKeepAliveSeconds(60);  // 스레드 재사용 시간
    executor.setThreadNamePrefix("AsyncExecutor-"); // 스레드 이름 prefix
    executor.initialize(); // ThreadPoolExecutor 생성

    return executor;
  }
}

 

2) 비동기를 적용할 메서드를 인터페이스로 정의

@Service
public interface AlertService {
    void createAlert(Long showInfoId);
    void sendMail(Alert alert) throws MessagingException;
}

 

 

2) 인터페이스를 구현하고 메서드를 오버라이딩해서 쓰레드 기능을 사용 가능

@Service
@RequiredArgsConstructor
public class EmailAlertService implements AlertService {
	
    private final JavaMailSender mailSender;

    @Override
    @Async("threadPoolTaskExecutor")
    public void sendMail(Alert alert) throws MessagingException {

        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
        mailSender.send(message);

    }
    
    @Override
    public void createAlert(Long showInfoId) {
        // 해당 공연정보에서 아티스트 관련 알림 객체 생성
        List<ShowArtist> showArtists = showArtistRepo.findByShowInfoId(showInfoId);
        generateArtistSubAlert(showArtists);

        List<ShowGenre> showGenres = showGenreRepo.findByShowInfoId(showInfoId);
        generateGenreSubAlert(showGenres);

        List<Alert> alerts = alertRepository.findByShowInfoId(showInfoId);
        for (Alert alert : alerts) {
            try {
                log.info("send email start");
                alert.setMessage(generateMessage(alert, alert.getUserNickname()));
                sendMail(alert);
            } catch (MessagingException e) {
                log.warn(e.getMessage());
                throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
            }
        }
    }

}

 

이렇게 설정하면 스프링 비동기 방식을 사용할수 있습니다.

 

비동기 vs 동기

5명의 구독자에게 동일한 메일을 발송해보는 실험을 통해 비동기 방식과 동기 방식이 얼마나 시간 차이가 발생하는지 알아보는 실험입니다. 아래는 동기 방식입니다.

 

먼저 동기 방식은 모든메일이 발송되고 공연 업로드 시간까지 총 소요시간이 15.37s가 걸렸습니다.

 

 

이어서 비동기 방식입니다.

 

비동기 방식은 단 439ms 만에 공연정보가 업로드 되었고 업로드 이후에 쓰레드로 이메일 전송이 차례로 이루어졌고 그 중간에 중단없이 다른요청을 수행할수 있음을 확인하였습니다. 앞으로 개발을 하면서 요청이 오래걸리는 API나 기능을 사용할 경우가 많을것인데 그때를 위해서 쓰레드를 사용한 스프링 비동기 방식은 개발자로서 알아야하는 필수 기술중에 하나라고 생각이 들었습니다.

 

마지막으로 알람서버로 분리돼면서 RabbitMQ와 연계해서 비동기방식을 사용한 방법에 대해서 설명드리겠습니다.

앞서 설명한 @EnableAsync 와 @Async 어노테이션을 사용해서 쓰레드 풀을 만드는것과 인터페이스를 구현하는것은 동일하고 단지 RabbitMQ의 큐에 메세지가 들어올때마다 메서드를 동작하게 하기위해 리스너 어노테이션을 추가해주고

메세지 처리 실패시에 재시도 루틴을 어노테이션으로 추가해서 비동기 방식과 연계해서 알람서버를 분리했습니다.

 

1) 알람 메세지를 보관하는 큐를 정의

@Configuration
public class AlarmConfig {
    @Bean
    public Queue queue(){
        return new Queue(
                "boot.amqp.alarm2-queue",
                true,
                false,
                true
        );
    }

    @Bean
    public Queue authQueue(){
        return new Queue(
                "boot.amqp.auth2-queue",
                true,
                false,
                true
        );
    }


}

 

2) 큐에서 메세지가 도착하는 이벤트를 듣는 @RabbitListener 와 재시도 루틴 @Retryable 어노테이션을 추가

@Service
@RequiredArgsConstructor
public class EmailAlertService implements AlertService {
    private JavaMailSenderImpl mailSender;
    // RabbitMQ 사용
    private final Gson gson;

    private Session session;
    private Transport transport;

    @Override
    // 메세지가 큐에 도착할때마다 메서드를 실행시킴
    @RabbitListener(queues = "boot.amqp.alarm2-queue")
    // 실패시 재시도 루틴(최대 3번, 10초 간격)
    @Retryable(value = MessagingException.class, maxAttempts = 3, backoff = @Backoff(delay = 10000))
    // 스프링 비동기 처리를 위함
    @Async("threadPoolTaskExecutor")
    public void sendMail(String rabbitMessage) throws MessagingException {
        try {
            log.info("===== email sending start =====");
            if (!transport.isConnected()) {
                transport.connect();
            }
            AlertDto alertDto = gson.fromJson(rabbitMessage, AlertDto.class);
            log.info(alertDto.toString());

            sendMail(alertDto);

        } finally {
            if (rabbitMessage == null || rabbitMessage.isEmpty()) {
                // 메시지 큐가 비어있으면 연결을 닫습니다.
                transport.close();
            }
        }
    }

 

 

정리

비동기 방식이 빠르고 좋아보이지만 서버의 리소스를 많이 사용하게 돼서 많은 메일을 한꺼번에 보낼경우 메인서버가 과부하될 가능성이 있습니다. 하지만 이렇게 서버를 분리하면 알람서버는 오직 알람 기능만 담당 하기 때문에 비동기 방식으로 인해 리소스 사용량이 잠시 많아지더라도 메인서버의 비지니스 로직에는 영향을 미치지 않습니다. 또한 메인서버는 더이상 비동기 방식과 알람 로직을 담당하지 않아도 돼서 더 최적화 되었습니다.