어제 오늘 내일

[Spring Boot] "회원가입 메일 보내느라 3초 걸린다고?" 비동기 처리(@Async)와 이벤트(Event) 기반 설계 본문

IT/SpringBoot

[Spring Boot] "회원가입 메일 보내느라 3초 걸린다고?" 비동기 처리(@Async)와 이벤트(Event) 기반 설계

hi.anna 2026. 3. 22. 10:47

 
개발하다 보면 "핵심 로직""부가 로직"이 섞일 때가 있습니다.
가장 대표적인 예가 회원가입입니다.

  1. 회원 정보 DB 저장 (핵심 - 0.1초)
  2. 가입 축하 메일 발송 (부가 - 3초)
  3. 가입 축하 쿠폰 발행 (부가 - 0.5초)

이걸 한 트랜잭션으로 묶어서 순차적으로 실행하면, 사용자는 가입 완료까지 총 3.6초를 기다려야 합니다. 만약 메일 서버가 죽으면? 가입 자체가 롤백되어 버리는 대참사가 일어납니다.
오늘은 이 "기다림"을 없애는 비동기 처리와, 서비스 간의 의존성을 끊어내는 이벤트 기반 설계를 알아보겠습니다.


1. 나중에 해! 비동기 처리 ()

가장 쉬운 해결책은 "메일 발송은 다른 쓰레드(Thread)가 알아서 하고, 넌 바로 응답해!"라고 하는 것입니다. 스프링은 어노테이션 하나로 이걸 지원합니다.

① 설정 켜기 ()

@EnableAsync // 비동기 기능 활성화
@SpringBootApplication
public class MyApplication { ... }

② 비동기 메서드 만들기

@Service
public class EmailService {

    @Async // 별도의 쓰레드에서 실행해라! (기다리지 않음)
    public void sendEmail(String email) {
        try {
            Thread.sleep(3000); // 3초 걸리는 작업 시뮬레이션
            System.out.println("메일 발송 완료: " + email);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

이제 join() 메서드에서 emailService.sendEmail()을 호출해도, 메인 쓰레드는 기다리지 않고 즉시 다음 줄로 넘어갑니다. 총 소요 시간: 0.1초!

주의사항: @Async프록시(Proxy) 기반으로 동작하므로, 같은 클래스 안의 메서드를 호출(Self-Invocation)하면 비동기가 작동하지 않습니다. 반드시 빈(Bean)으로 등록된 다른 클래스의 메서드를 호출해야 합니다.


2. 더 우아하게: 스프링 이벤트(Event) 도입

@Async로 속도는 해결했지만, 아직 문제가 남았습니다. 바로 강한 결합(Tight Coupling)입니다.

public class MemberService {
    // MemberService가 EmailService, CouponService를 다 알고 있어야 함 (의존성 지옥)
    private final EmailService emailService;
    private final CouponService couponService;

    public void join(Member member) {
        memberRepository.save(member);
        emailService.sendEmail(member.getEmail()); // 여기서 에러 나면?
        couponService.issueCoupon(member.getId()); // 여기서 에러 나면?
    }
}

나중에 "가입 시 SMS도 보내줘"라고 하면 MemberService 코드를 또 수정해야 합니다. 이건 OCP(개방-폐쇄 원칙) 위반입니다.

이걸 해결하려면 "가입 완료됐어!"라고 소리만 치고(Publish), 관심 있는 녀석들이 알아서 듣게(Listener) 만들면 됩니다.


3. 구현: 이벤트 발행과 구독

① 이벤트 클래스 정의 (DTO)

@Getter
@AllArgsConstructor
public class MemberJoinedEvent {
    private String email;
    private Long memberId;
}

② 이벤트 발행 (Publisher - MemberService)

이제 MemberService는 이메일이나 쿠폰 서비스따윈 모릅니다. 그냥 이벤트만 던집니다.

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    private final ApplicationEventPublisher publisher; // 이벤트 배달부

    @Transactional
    public void join(MemberDto dto) {
        Member member = memberRepository.save(dto.toEntity());

        // "회원 가입했대요~" 소리치기
        publisher.publishEvent(new MemberJoinedEvent(member.getEmail(), member.getId()));
    }
}

③ 이벤트 구독 (Listener - EmailService)

@Component
public class MemberEventListener {

    private final EmailService emailService;
    private final CouponService couponService;

    // 비동기로 처리해서 회원가입 속도도 지킴!
    @Async 
    @EventListener
    public void handleEmail(MemberJoinedEvent event) {
        emailService.sendEmail(event.getEmail());
    }

    @Async
    @EventListener
    public void handleCoupon(MemberJoinedEvent event) {
        couponService.issueCoupon(event.getMemberId());
    }
}

결과:

  1. MemberService는 이제 EmailService를 의존하지 않습니다. (코드 수정 없이 기능 추가 가능)
  2. @Async를 붙여서 메일 발송이 3초가 걸려도 회원가입은 0.1초 만에 끝납니다.

4. 꿀팁: 트랜잭션이 롤백된다면? ()

만약 DB 저장(save)은 성공해서 이벤트를 발행했는데, 그 뒤에 DB 트랜잭션이 롤백되었다면?
-> "회원가입은 실패했는데 가입 축하 메일은 날아가는" 황당한 상황이 발생합니다.

이때는 @EventListener 대신 @TransactionalEventListener를 사용하세요.

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleEmail(MemberJoinedEvent event) {
    // 트랜잭션이 확실히 '커밋(Commit)'된 후에만 실행됨!
    emailService.sendEmail(event.getEmail());
}

이렇게 하면 데이터 정합성까지 완벽하게 지킬 수 있습니다.


마치며

오늘의 결론입니다.

  1. 오래 걸리는 부가 작업은 @Async로 비동기 처리하여 응답 속도를 높인다.
  2. Spring Event를 사용하면 서비스 간의 의존성을 끊고(Decoupling) 유지보수성을 높일 수 있다.
  3. @TransactionalEventListener로 트랜잭션 성공 여부에 따라 이벤트를 제어하자.

이제 여러분의 서비스는 메일 서버가 터지든 말든, 회원가입만큼은 쾌속으로 처리하는 견고한 시스템(Resilient System)이 되었습니다.
다음 포스팅에서는 이 모든 서비스를 안정적으로 운영하기 위한 환경 구축, "내 컴퓨터에선 되는데..." 문제를 해결하는 Docker로 개발 환경 통일하기(Dockerfile & Compose)에 대해 알아보겠습니다.
도움이 되셨다면 좋아요와 댓글 부탁드립니다! 😊

반응형
Comments