스프링 이벤트
개요
예약이 확정되면 캘린더에 이벤트를 등록한다.
이 둘을 트랜잭션으로 묶었고 캘린더에 이벤트 등록이 실패하면 예약을 확정하는 상태로의 수정도 롤백된다.
어떻게 하면 좋을까?
이벤트
시청에 서류를 제출했다. 혼인신고서든 영업신고서든 뭐든.
-
A 직원이 서류를 처음 받는다. → 접수하고, 이상 없는지 검토한 뒤 → 처리가 끝나면 B 직원에게 넘긴다.
-
B 직원은 A가 넘긴 서류를 받아서 → 시스템에 최종 등록하고 → 처리가 완료되면 “신고가 완료되었습니다”라는 문자를 준다.
코드로 작성해보면…
public void submitReport(Long reportId) {
bService.sendSms(reportId);
}
보이는 바와 같이 A는 B가 어떤 일을 하는지 알고있다.
이로 인해 결합도가 증가한다.
업무가 늘어날 수록 A의 코드는 비대해지고 유지보수가 어려워진다.
어떻게 해결할 수 있을까?
- A 직원은 서류를 접수하고 이상이 없는 지 검토하고 검토가 완료됨을 선언한다.
- B 직원은 이를 기다리고 있다가 완료 소식을 듣고 가져와 자신의 일을 한다.
이렇게 되면 A 직원은 B직원의 역할을 몰라도 되고 비동기로 구현한다면 더 빠르게 자신의 할 일을 할 수 있다.
이 것이 스프링의 이벤트다.
코드
개념 | 역할 | 비유 (시청 예시) |
---|---|---|
ApplicationEventPublisher |
이벤트를 시스템에 발행(알리는) 역할 | “A 직원이 B에게 문서를 넘겨주세요!”라고 시청 전체 방송망에 공지하는 스피커 |
@EventListener |
발행된 이벤트를 듣고 처리하는 역할 | “혼인신고서 처리 완료 알림이 왔다!” 듣고 문자 보내는 B 직원 |
구체적으로는 위의 코드를 사용한다.
@Service
@RequiredArgsConstructor
public class DocumentService {
private final ApplicationEventPublisher publisher;
public void submitReport(Long reportId) {
System.out.println("A 직원: 혼인신고서 접수 완료!");
publisher.publishEvent(new MarriageReportSubmittedEvent(reportId));
}
}
여기서 publisher.publishEvent(new MarriageReportSubmittedEvent(reportId));
부분이 바로 이벤트를 알리는 부분이다.
public class MarriageReportSubmittedEvent {
private final Long reportId; // 어떤 신고서인지 구분용
private final String applicantName; // 신고한 사람 이름
private final String spouseName; // 배우자 이름
public MarriageReportSubmittedEvent(Long reportId, String applicantName, String spouseName) {
this.reportId = reportId;
this.applicantName = applicantName;
this.spouseName = spouseName;
}
public Long getReportId() {
return reportId;
}
public String getApplicantName() {
return applicantName;
}
public String getSpouseName() {
return spouseName;
}
}
보면 사실 DTO와 다를 게 없다. 그 이유는 다음과 같다.
- 이벤트 객체가 복잡하면 발행자가 리스너의 내부 구조까지 알게 돼서 결합도가 높아짐
- 최소한의 정보만 담고, 누가 듣는지는 신경 쓰지 않는 구조가 핵심
- POJO + 스프링 이벤트 시스템 → 유연하고 확장 가능한 구조 가능
이제 이걸 다음과 같이 이벤트를 받아서 처리한다.
@Component
public class SmsNotificationListener {
@EventListener
public void onMarriageReportSubmitted(MarriageReportSubmittedEvent event) {
System.out.printf("B 직원: %s님 혼인신고서 처리 완료. 문자 발송 중...\n",
event.getApplicantName());
}
}
비동기를 적용하려면 @Async 애노테이션을 붙여주면 된다.
이렇게 되면 원래의 스레드에서 벗어나 새로운 스래드에서 실행된다.
@Component
public class SmsNotificationListener {
@Async
@EventListener
public void onMarriageReportSubmitted(MarriageReportSubmittedEvent event) {
System.out.printf("B 직원: %s님 혼인신고서 처리 완료. 문자 발송 중...\n",
event.getApplicantName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("문자 발송 완료!");
}
}
이렇게 해서 처음의 결합도도 높고 트랜잭션이 분리되지 않아 상관없는 두 동작이 동시에 롤백되는 현상을 없앨 수 있다.