Skip to content

Domain Event on Springframework

정명주(myeongju.jung) edited this page Oct 14, 2017 · 6 revisions

Domain event sequence diagram example

Domain event sequence diagram example

Domain event configuration

도메인 이벤트가 발행가능하게 하는 환경

DomainEvent.java

/**
 * 도메인 이벤트 마커 인터페이스
 */
public interface DomainEvent {
}
  • 스프링에서 제공하는 org.springframework.context.ApplicationEvent를 상속 사용하는 것도 좋은 선택

개인적으로 상속을 별로 좋아하지 않아서 필요한 인터페이스를 정의해서 사용하는 것을 선호함

Events.java

public class Events {
    private static ThreadLocal<ApplicationEventPublisher> publisherLocal = new ThreadLocal<>();

    public static void publish(DomainEvent event) {
        if (event == null) {
            return;
        }
        if (publisherLocal.get() != null) {
            publisherLocal.get().publishEvent(event);
        }
    }

    static void setPublisher(ApplicationEventPublisher publisher) {
        publisherLocal.set(publisher);
    }


    static void reset() {
        publisherLocal.remove();
    }
}
  • 도메인 이벤트 편의 유틸리티

EventPublisherAspect.java

@Aspect
@Component
@Slf4j
public class EventPublisherAspect implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher publisher;
    private ThreadLocal<Boolean> appliedLocal = new ThreadLocal<>();

    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    public Object handleEvent(ProceedingJoinPoint joinPoint) throws Throwable {
        Boolean appliedValue = appliedLocal.get();
        boolean nested;
        if (appliedValue != null && appliedValue) {
            nested = true;
        } else {
            nested = false;
            appliedLocal.set(Boolean.TRUE);
        }
        if (!nested) Events.setPublisher(publisher);
        try {
            return joinPoint.proceed();
        } finally {
            if (!nested) {
                Events.reset();
                appliedLocal.remove();
            }
        }
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
        this.publisher = eventPublisher;
    }
}
  • ThreadLocal과 스프링의 @Aspect를 이용하여 트랜잭션 내에서 이벤트 발행 및 구독이 가능하게 처리하는 컴포넌트

Publish, Subscribe and Event

실제 이벤트를 발행하고 구독

PartnerBlockedEvent.java

/**
 * 협력사 차단 이벤트
 * <p>
 *     1. 관리하는 업체를 상위 협력사로 이관
 * </p>
 * @see PartnerEventHandler#handle(PartnerBlockedEvent)
 */
@Value
public class PartnerBlockedEvent implements DomainEvent {
    @NonNull
    private Partner blockedPartner;
    @NonNull
    private User publishUser;
}
  • 실제 구상 도메인 이벤트를 정의

Partner.java

@Entity
public class Partner implements User {
    // ...
    /**
     * 현 협력사를 차단
     * @param user 차단하는 사용자
     */
    public void block(User user) {
        // 자기자신을 차단할 수 없음
        if (partnerNo.equals(user.getPartnerNo())) {
            throw new ImpossibleBlockException("Can't block myself : " + userId);
        }
        this.block = true;
        this.cau.update(user);    // 마지막 수정자, 수정일시 변경
        // 차단 도메인 이벤트 발행 !!!
        Events.publish(new PartnerBlockedEvent(this, user));
    }
}
  • Entity(도메인 객체) 내에서 실제 구상 이벤트를 발행!!!

PartnerEventSubscriber.java

/**
 * 협력사 도메인 이벤트 구독 컴포넌트
 */
@Component
@Slf4j
public class PartnerEventSubscriber {
    private final CompanyCommander companyCommander;

    @Autowired
    public PartnerEventSubscriber(CompanyCommander companyCommander) {
        this.companyCommander = companyCommander;
    }

    /**
     * 협력사 차단 이벤트 구독 처리 - 동일 트랜잭션 내 처리(BEFORE_COMMIT)
     * <p>
     *     협력사가 관리하는 하위 업체들을 상위 업체들로 이관
     * </p>
     * @param event 협력사 차단 이벤트
     */
    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    // @Async 비동기처리 가능
    public void subscribe(PartnerBlockedEvent event) {
        // 차단된 협력사
        Partner blockedPartner = event.getBlockedPartner();
        companyCommander.moveCompanies(blockedPartner);
        // ...
        // 구독 내 이벤트 발행이므로 정상적으로 발행되지 않음 !!!
        anotherPatner.block(event.getPublishUser());
    }
}
  • 실제 구상 도메인 이벤트를 구독해서 처리
  • 필요에 따라서 같은 트랜잭션 내에서 처리하거나 트랜잭션 외(AFTER_COMMIT)로 처리 할 수 있음
    • 위와 같은 경우에는 같은 트랜잭션이지만 Mail, SMS 발송의 경우에는 트랜잭션 외로 처리하면 좋을 것 같다.
  • 재미있는 점은 이벤트 구독 컴포넌트 내에서 다시 이벤트를 발행하면 정상적으로 발행되지 않는다.
  • handle 메소드에 @Async를 달면 비동기로 처리된다.
    • 물론 @EnableAsync와 같은 환경설정이 우선 되야한다.
    • 트랜잭션 내 처리도 가능하다고 하나 개인적으로 트랜잭션 내에서 처리가 요구되면 동기적(non-async)으로 처리하는 것이 좋을 것 같다.
    • 비동기는 아무래도 트랜잭션의 영향을 받지 않는 Mail, SMS 발송의 경우에 사용하면 좋을 것 같다.

Reference

PlanUml

Domain event sequence diagram example

@startuml
autonumber@startuml
autonumber
actor Client
entity Partner
participant  PartnerBlockedEvent
control PartnerEventSubscriber
Client->Partner:block()
activate Partner
Partner->Partner:domain logic
Partner->PartnerBlockedEvent:new
activate PartnerBlockedEvent
Partner<-PartnerBlockedEvent
deactivate PartnerBlockedEvent
Partner-->PartnerEventSubscriber:publish event
activate PartnerEventSubscriber
PartnerEventSubscriber->CompanyCommander:moveCompanies()
activate CompanyCommander
PartnerEventSubscriber<-CompanyCommander
deactivate CompanyCommander
deactivate PartnerEventSubscriber
deactivate Partner
@enduml
Clone this wiki locally