본문 바로가기
Framework/Spring Boot

[Spring Boot] 비동기(@Async)와 스케줄링(@Scheduled): 무거운 작업을 뒤로 빼라

반응형

여러분이 야심 차게 런칭한 서비스의 회원가입 로직을 짜고 있습니다. 1. 유저 정보 DB 저장 -> 2. 포인트 지급 -> 3. 환영 이메일 발송 순서로 코드를 짰습니다. DB 저장은 0.1초면 끝나지만, 외부 메일 서버를 거치는 '이메일 발송'은 무려 3초가 걸립니다. 사용자가 [가입하기] 버튼을 누르면 이 모든 작업이 '직렬(동기)'로 실행되어 브라우저 화면이 3초 넘게 하얗게 멈춰있게 됩니다. 성질 급한 한국 유저들은 서버가 죽은 줄 알고 새로고침을 연타하다가 결국 이탈해 버릴 것입니다. 이럴 때 백엔드 개발자의 센스가 발휘되어야 합니다. "이메일 발송 같은 건 메인 응답에 영향을 주지 않도록 다른 쓰레드(알바생)에게 짬처리(?) 시키고, 나는 유저에게 0.1초 만에 '가입 성공!'을 외쳐주자!" 이것이 바로 비동기 처리(Asynchronous)의 핵심 철학입니다. 스프링 부트(Spring Boot)는 이 복잡한 멀티 쓰레딩 작업을 @Async 어노테이션 하나로 퉁쳐버리는 엄청난 마법을 제공합니다.

이번 [Spring Boot 실무 완벽 가이드] 시리즈에서는 "빠릿빠릿한 API 스피드를 위한 필수 스킬!" @Async 비동기 처리와 @Scheduled를 활용한 배치(Batch) 스케줄링 완벽 실무 가이드에 대하여 다뤄보겠습니다.

⚡ 1. @EnableAsync: 비동기 엔진 활성화하기

비동기를 쓰려면 가장 먼저 스프링 부트 애플리케이션에 "지금부터 비동기 엔진을 켤게!"라고 선언해야 합니다. 그리고 무작정 켜기만 하면 스프링이 요청이 올 때마다 끝도 없이 쓰레드(알바생)를 무한 생성하여 메모리가 터질 수 있으므로, 쓰레드 풀(Thread Pool, 알바생 대기실)을 명시적으로 설정해 주는 것이 실무 기본기입니다.

@EnableAsync // 🌟 비동기 기능 활성화의 핵심!
@Configuration
public class AsyncConfig {

    // 우리가 통제할 수 있는 쓰레드 풀(알바생 대기실)을 만듭니다.
    @Bean(name = "mailExecutor")
    public Executor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);  // 기본으로 대기할 최소 알바생 수 (5명)
        executor.setMaxPoolSize(30);  // 일감이 폭주하면 최대 고용할 수 있는 알바생 수 (30명)
        executor.setQueueCapacity(100); // 알바생이 다 바쁠 때 일감을 쌓아둘 대기줄(큐) 크기 (100개)
        executor.setThreadNamePrefix("Mail-Async-"); // 로그에 찍힐 쓰레드 이름
        executor.initialize();
        return executor;
    }
}

🚀 2. @Async의 마법: 메인 쓰레드는 퇴근해!

설정이 끝났으면 이메일 발송 메서드 위에 @Async를 달아주기만 하면 끝입니다. 그러면 메인 톰캣(Tomcat) 쓰레드는 이 메서드를 호출하는 즉시, 일을 백그라운드 쓰레드에게 던져버리고 뒤도 안 돌아보고 자신의 길을(클라이언트 응답) 떠납니다.

@Service
@RequiredArgsConstructor
public class EmailService {

    // 🌟 이 어노테이션이 붙은 메서드는 메인 쓰레드가 멈춰 서서 기다리지 않고 즉시 통과해 버립니다!
    // 아까 설정한 "mailExecutor" 알바생 그룹이 3초가 걸리든 10초가 걸리든 뒤에서 알아서 처리합니다.
    @Async("mailExecutor")
    public void sendWelcomeEmail(String email) {
        try {
            // 이메일 발송에 3초가 걸린다고 가정합시다.
            Thread.sleep(3000); 
            log.info("📩 {} 님께 환영 이메일 발송 완료!", email);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

※ 주의 (AOP 프록시 함정): 이전 17단계 @Transactional과 마찬가지로 @Async 역시 프록시(Proxy) 기반으로 동작합니다. 따라서 같은 클래스 내부에서 자기 자신의 메서드를 호출하면 비동기가 씹히고 동기로 작동해버리는 함정이 똑같이 존재합니다! 비동기 메서드는 반드시 '다른 클래스'로 빼서 호출해야 합니다.


⏰ 3. @Scheduled: 서버 안의 작은 크론(Cron) 잡

"매일 자정(밤 12시)마다 기간이 만료된 쿠폰을 일괄 삭제해라", "1시간마다 방문자 통계를 갱신해라" 같은 주기적인 백그라운드 작업은 어떻게 처리할까요? 리눅스의 crontab을 써도 되지만, 스프링 부트 안에서는 @Scheduled 하나로 코드로 관리할 수 있습니다.

@EnableScheduling // 🌟 스케줄링 마법 활성화 (보통 설정 클래스나 Application 클래스에 붙임)
@Service
public class CouponBatchService {

    // 1. Fixed Rate: 이전 작업이 안 끝났어도 무조건 5초(5000ms)마다 무자비하게(?) 실행!
    @Scheduled(fixedRate = 5000)
    public void runEvery5Seconds() {
        log.info("5초마다 실행되는 핑(Ping) 테스트...");
    }

    // 2. Cron Expression: 실무에서 가장 많이 쓰는 크론 표현식! (초 분 시 일 월 요일)
    // 아래 설정은 "매일 밤 자정(0초 0분 0시)에 실행해라!" 라는 뜻입니다.
    @Scheduled(cron = "0 0 0 * * *")
    public void deleteExpiredCoupons() {
        log.info("🧹 매일 자정, 만료된 쿠폰 일괄 정리 배치 작업 시작!");
        // DB 쿠폰 정리 로직...
    }
}

🎯 4. 마무리 및 다음 단계

지금까지 클라이언트의 인내심을 갉아먹는 무거운 I/O 작업(이메일, 알림톡, 통계 등)을 백그라운드 쓰레드 풀로 우아하게 던져버려 응답 스피드를 극한으로 끌어올리는 @Async 비동기 처리와, 주기적인 작업을 코드로 깔끔하게 관리하는 @Scheduled 스케줄링 기법에 대해 다루어 보았습니다. 이제 여러분의 서버는 지연 없는 쾌적한 핫스팟이 되었습니다.

비즈니스 로직도 완벽하고, 속도도 비동기로 빠르고 훌륭합니다. 이제 유저들이 신나서 회원가입을 하고 자신의 '프로필 이미지 사진'을 올리려고 합니다. 이때 사진 파일(JPEG, PNG)을 서버의 디스크(하드 드라이브)에 직접 저장하면 어떻게 될까요? 나중에 사용자가 늘어나서 서버를 3대로 늘리면(Scale-out), 1번 서버에 저장된 이미지가 2번 서버에는 없어서 사진이 깨지는 치명적인 '엑스박스' 참사가 벌어집니다! 이어지는 19단계 포스팅에서는 "이미지와 영상은 서버가 아니라 클라우드 창고에 쑤셔 넣어라!" 현대 백엔드 파일 업로드의 글로벌 스탠다드, AWS S3 버킷(Bucket) 연동과 MultipartFile 처리 실무 아키텍처에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!

반응형