| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- junit5
- javascript
- junit
- HashMap
- java테스트
- Array
- 단위테스트
- 인텔리제이
- html
- Visual Studio Code
- list
- 자바
- Eclipse
- math
- string
- 배열
- 자바문법
- ArrayList
- CSS
- Java
- 정규식
- vscode
- 자바스크립트
- 문자열
- 스프링부트
- js
- input
- 테스트자동화
- SpringBoot
- IntelliJ
- Today
- Total
어제 오늘 내일
[Spring Boot] "회원가입 메일 보내느라 3초 걸린다고?" 비동기 처리(@Async)와 이벤트(Event) 기반 설계 본문
[Spring Boot] "회원가입 메일 보내느라 3초 걸린다고?" 비동기 처리(@Async)와 이벤트(Event) 기반 설계
hi.anna 2026. 3. 22. 10:47
개발하다 보면 "핵심 로직"과 "부가 로직"이 섞일 때가 있습니다.
가장 대표적인 예가 회원가입입니다.
- 회원 정보 DB 저장 (핵심 - 0.1초)
- 가입 축하 메일 발송 (부가 - 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());
}
}
결과:
MemberService는 이제EmailService를 의존하지 않습니다. (코드 수정 없이 기능 추가 가능)@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());
}
이렇게 하면 데이터 정합성까지 완벽하게 지킬 수 있습니다.
마치며
오늘의 결론입니다.
- 오래 걸리는 부가 작업은
@Async로 비동기 처리하여 응답 속도를 높인다. - Spring Event를 사용하면 서비스 간의 의존성을 끊고(Decoupling) 유지보수성을 높일 수 있다.
@TransactionalEventListener로 트랜잭션 성공 여부에 따라 이벤트를 제어하자.
이제 여러분의 서비스는 메일 서버가 터지든 말든, 회원가입만큼은 쾌속으로 처리하는 견고한 시스템(Resilient System)이 되었습니다.
다음 포스팅에서는 이 모든 서비스를 안정적으로 운영하기 위한 환경 구축, "내 컴퓨터에선 되는데..." 문제를 해결하는 Docker로 개발 환경 통일하기(Dockerfile & Compose)에 대해 알아보겠습니다.
도움이 되셨다면 좋아요와 댓글 부탁드립니다! 😊