DevBoi

[DDD] Notification and Event 본문

Develop/[DDD]

[DDD] Notification and Event

HiSmith 2023. 6. 19. 16:32
반응형

생소한 단어들일 수 있다. DDD를 하지 않으면, 잘 모른다.

우선, 공부를 해보자. Notification은 전파, 전달에 대한 기능을 구현하는 모듈이다.

 

Event가 발생했을때, Notification을 주어서, 특정 이벤트를 발생시키는것이다.

Outbox패턴으로 작업한다면, 이벤트를 저장하고, 배치나 스케줄러를 통해서 적정 단위로 처리된다.

 

 

이벤트 소싱과 Notification은 다르다.

Notification이란, Repository에서 애그리게잇을 가져올때 이벤트를 사용해서 재구성한다.

재구성 하기 위해서는 이벤트 소싱이 가장 중요하다.

 

특정이벤트를 관심있어 하는 주체에 대해, Notification이 발동된다.

Notification은 이벤트 소싱에 저장된 일련의 애그리게잇, 주체에게 이벤트를 전달해서 핸들링한다.

'저장된 이벤트의 전달을 위한 아키텍처 스타일이다. '

 

EventStore는 왜 필요할까?

이벤트 스토어가 없는 경우이다.

데이터베이스에 이벤트를 저장하고, 이벤트를 발행하는 작업이 동시에 이루어지고

publisher를 통해서 해당 이벤트를 관심있어 하는 주체에게 전달해준다.

이렇게 나누게 되면, 해당 롤백 타이밍을 잡기도 어렵고, 디버깅도 힘들다.

 

중간과정에서 이슈가 생겼다고 가정을 해보자.

그러면 다른트랜잭션을 롤백하거나, 같은 트랜잭션으로 묶는다고해도 전파 범위를 다 같이 맞춰주어야 한다.

 

그래서, 이를 바로, publish로 날리는 것이 아니라, Event Store에 저장하는 것이다.

또한 Nofitication publisher는 각각 이벤트 스토어를 바라보고, 이는 스케줄러를 통해서, 이벤트가 전달 되게 된다.

이렇게 되면, 각각의 이벤트 스토어에서 저장된 값들을 스케줄러로 빼오고,

해당 관련된 퍼블리셔만 값을 가지고 이벤트 알림을 하게 된다.

 

대충의 소스를 짜보도록 하자.

 

먼저 Event한개를 추상화 한다. 단순히 해당 이벤트 아이디정도만존재한다.

package com.smith.PqrsFramework;

import java.util.UUID;

public interface Event {
  UUID eventId();
}

그리고 해당 인터페이스를 구현하는 추상 클래스를 한개 생성한다.

public abstract class DomainEvent implements Event{
  private final UUID eventId;
  protected  DomainEvent(){
    this.eventId = UUID.randomUUID();
  }
}

해당 내용중 protected 생성자로 생성을 한 이유는 아래와 같다.

private 생성자와 기능은 거의 동일하다, 외부에서 생성자를 못만들게 하는것이다.

즉 제약을 두는 것이다.

 

private 가 아니라 protected로 하는 이유는, JPA 에서 로딩 시점에 따라 프록시 객체를 사용하는데,

해당 프록시 객체는 기본생성자를 바탕으로한다.

만약에 private 생성자로 생성한다면, JPA가 프록시객체를 기본생성자로 생성할 수 없게 된다.

따라서, protected 생성자로 생성해준다.

 

무튼 해당 이벤트를 기점으로 추가로 소스를 짠다.

 

Domain에 대한 registry 를 우선 생성한다.

@Component
public class DomainRegistry implements ApplicationContextAware {
  private static ApplicationContext applicationContext;
  public static ApplicationEventPublisher eventPublisher(){
    return this.applicationContext;
  }
  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
      DomainRegistry.applicationContext = applicationContext;
    }
    public void makeWork(){
    Order order = applicationContext.getBean("OrderFactoryBean",Order.class);
    order.notify();
    }
  }

해당 내용에서 생소한 부분은 2개 정도있을것이다.

ApplicationContextAware, applicationEventPublisher

 

1) ApplicationContextAware 

해당 인터페이스는 동적으로 빈들을 전달 받을 때 유용한 인터페이스이다.

id를 기반으로 빈을 동적으로 전달 받고싶을때, 전달 받을 수 있는 인터페이스이다.

즉 해당 구현체가 초기화 될때 ApplicationContext를 전달 받는다.

이렇게 되면, 해당 DomainRegistry에서 컨텍스트를 받아와서 사용할 수도있고, context에서 다른 빈을 참조해서 사용할 수도있다.

 

말도안되지만, 예시를 makeWork로 만들어봤다.

 

유사하게는 이게 있다.

BeanNameAware

public class MyBeanName implements BeanNameAware {

    @Override
    public void setBeanName(String beanName) {
        System.out.println(beanName);
    }
}
@Configuration
public class Config {

    @Bean(name = "myCustomBeanName")
    public MyBeanName getMyBeanName() {
        return new MyBeanName();
    }
}

 

이런식으로, MyBeanName을 활용하면, 특정 빈의 이름을 가져올 수 있다.

뭐.. 무튼 나중에 좀더 동작방식을 알아보자

 

2) ApplicationEventPublisher

applicationcontext가 상속하는 인터페이스중 하나이고, 옵저버 패턴의 구현체이다.

이벤트를 옵저버에게 전달 하고자할때, 그리고 결합도를 낮추고자 할때 용이하다.

다시 말하면 Publisher와 Observer의 결합도는 낮추되, 이벤트는 전달하고싶을때 사용하면 된다.

 

예를 들어서, 하나의 주문 모듈을 만들어보자

@Getter
@Builder
public class OrderEvent{
  private String eventId;
  private String types;
  private Map<String,String> data;
  public static OrderEvent toComplete(Map data){
    return  OrderEvent.builder()
      .eventId(UUID.randomUUID().toString())
      .types("create")
      .build();
  }
  public static OrderEvent toCancel(Map data){
    return OrderEvent.builder()
      .eventId(UUID.randomUUID().toString())
      .types("cancel")
      .build();
  }
}

 

특정 이벤트 상황에 따라, 이벤트를 리턴해주는 이벤트 생성 메소드가 있다.

그 다음은 퍼블리셔를 만들어보자

@Component
@RequiredArgsConstructor
public class OrderPublisher {
  private final ApplicationEventPublisher applicationEventPublisher;
  public void notifyComplete(OrderEvent orderEvent){
    applicationEventPublisher.publishEvent(orderEvent);
  }
  public void notifyCancel(OrderEvent orderEvent){
    applicationEventPublisher.publishEvent(orderEvent);
  }
}

 

마지막으로 이벤트를 퍼블리셔가 누구에게 전달해야할지 알아야하기 떄문에

이벤트 핸들러를 작성해준다.

@Component
public class OrderEventHandler {
  
  @EventListener
  public void onOrderEventHandler(OrderEvent orderEvent){
    
  }
}

그리고 해당 이벤트를 생성해서, 퍼블리셔에 전달해주는 서비스 로직까지 구현해보자

@Service
@RequiredArgsConstructor
public class OrderService {
  private final OrderPublisher orderPublisher;

  public void orderEvent(){
    try{
      Map map = new HashMap();
      map.put("gd","dfd");
      OrderEvent event = OrderEvent.toComplete(map);
      orderPublisher.notifyComplete(event);
    }catch(Exception e){

    }
  }
}

이러고 컨트롤러에서 호출하게 되면 정상적으로 이벤트를 주고 받을 수 있다.

 

서론이 길었다.

무튼 이렇게 설계를 하면, 이벤트 생성 및 publish가 가능해진다.

 

이런 이벤트를 추가하는 로직은, 공통으로 묶을 수 있다. 하나의 추상 Root를 만들어보자

이렇게 하면, 모든 애그리거트는  해당 로직을 상속 받을 수 있고, 메서드를 사용할 수 있다.

public abstract class RootAggregate {
  public void addEvent(DomainEvent domainEvent){
  ApplicationEventPublisher publisher = DomainRegistry.eventPublisher();
  publisher.publishEvent(domainEvent);
  }
}

 

특정 서비스에서 RootAggreagate를 상속받고

DoaminEvent를 특정 이벤트가 상속 받으면, 해당 상속체에서 addEvent로 전달할 수 있고

해당 이벤트를 처리하는 이벤트 핸들러를 찾아서 퍼블리셔가 잘 전달 해주는 것을 알 수 있다.

 

쉽게 얘기하면

이벤트 발생 로직 -> 이벤트 리스너 -> 퍼블리셔에게 이벤트 퍼블리쉬, 방식으로 이벤트를 발행하고, 전파한다.

아웃 박스 형식으로 하면 배치나 스케줄러랑 같이 쓰게 되는데 이건 좀 나중에 보도록하고

 

우선, 알림과 이벤트는 여기서 종-결

반응형