개요

예약이 확정되면 캘린더에 이벤트를 등록한다.
이 둘을 트랜잭션으로 묶었고 캘린더에 이벤트 등록이 실패하면 예약을 확정하는 상태로의 수정도 롤백된다.
어떻게 하면 좋을까?

이벤트

시청에 서류를 제출했다. 혼인신고서든 영업신고서든 뭐든.

  1. A 직원이 서류를 처음 받는다. → 접수하고, 이상 없는지 검토한 뒤 → 처리가 끝나면 B 직원에게 넘긴다.

  2. B 직원은 A가 넘긴 서류를 받아서 → 시스템에 최종 등록하고 → 처리가 완료되면 “신고가 완료되었습니다”라는 문자를 준다.

코드로 작성해보면…

public void submitReport(Long reportId) {
    bService.sendSms(reportId); 
}

보이는 바와 같이 A는 B가 어떤 일을 하는지 알고있다.
이로 인해 결합도가 증가한다.
업무가 늘어날 수록 A의 코드는 비대해지고 유지보수가 어려워진다.

어떻게 해결할 수 있을까?

  1. A 직원은 서류를 접수하고 이상이 없는 지 검토하고 검토가 완료됨을 선언한다.
  2. 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("문자 발송 완료!");
    }
}

이렇게 해서 처음의 결합도도 높고 트랜잭션이 분리되지 않아 상관없는 두 동작이 동시에 롤백되는 현상을 없앨 수 있다.