본문 바로가기
도메인 주도 설계

Notification 구현 in IDDD_Samples - 리소스풀 방식

by simplify-len 2020. 8. 8.

 

- 책에서 말하는 Notification 구현(리소스풀 방식)
- 실제 프로젝트에서 Notification 구현
- 그림으로 이해해보기

 

앞서 설명했던 메세지 전달 방법중 리소스풀을 활용한 전달 방법

책에서 말하는 Notifiaction 구현

책에서 시작은 NotificationService에 대한 언급으로 시작한다.

이벤트가 모델로부터 발생한 알림으로써 발행될 때, 도메인의 관심사가 아닌 애플리케이션의 관심사라는 것을 강조.

public class NotificationService {

    @Autowired
    private EventStore eventStore;

    @Autowired
    private NotificationPublisher notificationPublisher;

    public NotificationApplicationService() {
        super();
    }

    @Transactional(readOnly=true)
    public NotificationLog currentNotificationLog() {
		...
    }

    @Transactional(readOnly=true)
    public NotificationLog notificationLog(String aNotificationLogId) {
		...
    }

    @Transactional
    public void publishNotifications() {
		...
    }

    protected EventStore eventStore() {
        return this.eventStore;
    }

    protected NotificationPublisher notificationPublisher() {
        return this.notificationPublisher;
    }
}

처음 두 메서드는 클라이언트에게 레스트폴 리소스로 제공되는 NotificationLog 인스턴스를 쿼리하기 위해 사용되며, 세 번째 메소드는 메시징 메커니즘을 통해 개발 Notification 인스턴스를 발행하기 위해 사용된다.

 이 팀은 먼저 NotificationLog 인스턴스를 가져오는 쿼리에 집중하고, 그다음에 메시징 인프라와 상호작용하는 부분으로 관심사를 옮긴다.

NotificationLog의 발행

 @Transactional(readOnly = true)
    public NotificationLog currentNotificationLog() {
        EventStore eventStore = EventStore.instance();

        return this.findNotificationLog(
                this.calculateCurrentNotificationLogId(eventStore), eventStore);
    }

    @Transactional(readOnly = true)
    public NotificationLog notificationLog(String aNotificationLogId) {
        EventStore instance = EventStore.instance();

        return this.findNotificationLog(
                new NotificationLogId(aNotificationLogId), instance);
    }

위 두개의 메소드는 NotificationLog를 찾아야만 한다.

이는 결국 이벤트 저장소로부터 DomainEvent 인스턴스가 직렬화된 부분을 찾아서 각 인스턴스를 Notification으로 캡슐화하고, 그 결과를 NotificationLog로 모은다.

 NotificationLog인스턴스가 생성되면, 이를 헤스트풀 리소스로 나타내 요청하는 클라이언트에게 제공한다.

현재 로그는 멈추지 않고 움직이는 대상으로 볼 수 있기 때문에 반드시 요청할 때마다 그 식별자를 계산해야 한다. 아래 코드는 그것과 과련된 내용이다.

private NotificationLogId calculateCurrentNotificationLogId(EventStore anEventStore) {
        long count = anEventStore.countStoredEvents();

        long remainder = count % NOTIFICATIONS_PER_LOG;

        if (remainder == 0 && count > 0) {
            remainder = NOTIFICATIONS_PER_LOG;
        }

        long low = count - remainder + 1;

		// 아직 알림이 완전히 가득 차지 않은 상황에서도 만들어진 식별자의 값을 보장해준다.
        long high = low + NOTIFICATIONS_PER_LOG - 1;

        return new NotificationLogId(low, high);
    }

위 로직의 NotificationLogId 는 단순히 식별자의 최소값과 최대값의 범위를 캡슐화한것이다. 

public class NotificationLogId {
	...
    public NotificationLogId(String aNotificationLogId) {
            super();

            String[] textIds = aNotificationLogId.split(",");
            this.setLow(Long.parseLong(textIds[0]));
            this.setHigh(Long.parseLong(textIds[1]));
        }
    ...
 }

 

NotificationLogId로 반환된다는 의미는 어떤 것인가?

저장된 로그의 최소값과 최대값을 가진다는 것을 의미한다.

public class NotificationService {

	...

 private NotificationLog findNotificationLog(NotificationLogId notificationLogId, 
                                                EventStore eventStore) {
        List<StoredEvent> storedEvents =
                eventStore.allStoredEventsBetween(notificationLogId.low(), notificationLogId.high());
        
        long count = eventStore.countStoredEvents();
        
        boolean archivedIndicator = notificationLogId.high() < count;
        
        NotificationLog notificationLog = new NotificationLog(
                notificationLogId.encoded(),
                NotificationLogId.encoded(notificationLogId.next(
                        LOG_NOTIFICATION_COUNT)),
                NotificationLogId.encoded(notificationLogId.previous(
                        LOG_NOTIFICATION_COUNT)),
                this.notificationFrom(storedEvents),
                archivedIndicator);
        
        return notificationLog;

    }

    private List<Notification> notificationFrom(List<StoredEvent> eventStore) {
        List<Notification> notifications =
                new ArrayList<>(eventStore.size());
        
        for (StoredEvent storedEvent: eventStore){
            DomainEvent domainEvent = EventStore.toDomainEvent(storedEvent);
            
            Notification notification =
                    new Notification(
                            domainEvent.getClass().getSimpleName(),
                            storedEvent.eventId(),
                            domainEvent.occurredOn(),
                            domainEvent
                    );
            notifications.add(notification);
        }
        return notifications;
    }
    ...
 }

Notification 인스턴스나 전체 로그를 전혀 저장할 필요가 없다는 사실은 꽤 흥미롭다. 우린 필요할 때마다 이를 만들어 낼수 있다. 이런 이유로, 요청 시점에서 NotificationLog를 캐싱하면 성능과 확장성에 도움을 받을 수 있다.

//EventStore.java

    @Override
    @SuppressWarnings("unchecked")
    public List<StoredEvent> allStoredEventsBetween(long aLowStoredEventId, long aHighStoredEventId) {
        Query query =
                this.session().createQuery(
                        "from StoredEvent as _obj_ "
                        + "where _obj_.eventId between ? and ? "
                        + "order by _obj_.eventId");

        query.setParameter(0, aLowStoredEventId);
        query.setParameter(1, aHighStoredEventId);

        List<StoredEvent> storedEvents = query.list();

        return storedEvents;
    }

EventStore에서 쿼리를 보내서 결과를 가져온다.

마지막으로, 웹 티어에서 현재 로그와 보관된 로그를 발행한다.

	@GET
    @Produces({ OvationsMediaType.ID_OVATION_TYPE })
    public Response getCurrentNotificationLog(
            @Context UriInfo aUriInfo) {

        NotificationLog currentNotificationLog =
            this.notificationApplicationService()
                .currentNotificationLog();

        if (currentNotificationLog == null) {
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }

        Response response =
            this.currentNotificationLogResponse(
                    currentNotificationLog,
                    aUriInfo);

        return response;
    }

    @GET
    @Path("{notificationId}")
    @Produces({ OvationsMediaType.ID_OVATION_TYPE })
    public Response getNotificationLog(
            @PathParam("notificationId") String aNotificationId,
            @Context UriInfo aUriInfo) {

        NotificationLog notificationLog =
            this.notificationApplicationService()
                .notificationLog(aNotificationId);

        if (notificationLog == null) {
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }

        Response response =
            this.notificationLogResponse(
                    notificationLog,
                    aUriInfo);

        return response;
    }

실제 프로젝트에서 Notification 구현

실제 프로젝트에서는 Factory를 활용해 캡슐화와 책임을 위임했다. 아래 코드를 분석하면서 정리한 내용을 첨부한다.

NotificationApplicationService.java

public class NotificationApplicationService {

    @Autowired
    private EventStore eventStore;

    @Autowired
    private NotificationPublisher notificationPublisher;

    public NotificationApplicationService() {
        super();
    }

    @Transactional(readOnly=true)
    public NotificationLog currentNotificationLog() {
        NotificationLogFactory factory = new NotificationLogFactory(this.eventStore());

        return factory.createCurrentNotificationLog();
    }

    @Transactional(readOnly=true)
    public NotificationLog notificationLog(String aNotificationLogId) {
        NotificationLogFactory factory = new NotificationLogFactory(this.eventStore());

        return factory.createNotificationLog(new NotificationLogId(aNotificationLogId));
    }

    @Transactional
    public void publishNotifications() {
        this.notificationPublisher().publishNotifications();
    }

    protected EventStore eventStore() {
        return this.eventStore;
    }

    protected NotificationPublisher notificationPublisher() {
        return this.notificationPublisher;
    }
}

 

책에서 나타낸것과 다른 부분은 NotificationLogFactory 부분이다. 책에서는 currentNotificationLog 메서드의 구현을 바로 진행했으나, 프로젝트에서는 NotificationLogFactory에게 위임했다.

public class NotificationLogFactory {

    // this could be a configuration
    private static final int NOTIFICATIONS_PER_LOG = 20;

    private EventStore eventStore;

    public static int notificationsPerLog() {
        return NOTIFICATIONS_PER_LOG;
    }

    public NotificationLogFactory(EventStore anEventStore) {
        super();

        this.setEventStore(anEventStore);
    }

    public NotificationLog createCurrentNotificationLog() {
        return this.createNotificationLog(
                this.calculateCurrentNotificationLogId(eventStore));
    }

    public NotificationLog createNotificationLog(
            NotificationLogId aNotificationLogId) {

        long count = this.eventStore().countStoredEvents();

        NotificationLogInfo info = new NotificationLogInfo(aNotificationLogId, count);

        return this.createNotificationLog(info);
    }
}

이렇게 위와같이 Factory한테 역할을 위임 후에 createNotificationLog(NotificationLogId aNotificationLogId) 를 통해 책에서 말하는 findLogNotification 의 역할을 수행합니다.

// NotificationFactoryLog.java

private NotificationLog createNotificationLog(
            NotificationLogInfo aNotificationLogInfo) {

        List<StoredEvent> storedEvents =
            this.eventStore().allStoredEventsBetween(
                    aNotificationLogInfo.notificationLogId().low(),
                    aNotificationLogInfo.notificationLogId().high());

        boolean archivedIndicator =
                aNotificationLogInfo.notificationLogId().high() < aNotificationLogInfo.totalLogged();

        NotificationLogId next = archivedIndicator ?
                aNotificationLogInfo.notificationLogId().next(NOTIFICATIONS_PER_LOG) :
                null;

        NotificationLogId previous =
                aNotificationLogInfo.notificationLogId().previous(NOTIFICATIONS_PER_LOG);

        NotificationLog notificationLog =
            new NotificationLog(
                    aNotificationLogInfo.notificationLogId().encoded(),
                    NotificationLogId.encoded(next),
                    NotificationLogId.encoded(previous),
                    this.notificationsFrom(storedEvents),
                    archivedIndicator);

        return notificationLog;
    }

    private List<Notification> notificationsFrom(List<StoredEvent> aStoredEvents) {
        List<Notification> notifications =
            new ArrayList<Notification>(aStoredEvents.size());

        for (StoredEvent storedEvent : aStoredEvents) {
            DomainEvent domainEvent = storedEvent.toDomainEvent();

            Notification notification =
                new Notification(storedEvent.eventId(), domainEvent);

            notifications.add(notification);
        }

        return notifications;
    }

 

그 뒤에 로직은 앞서 보여드렸던, 책에서 말하는 Notification 구현과 동일하다. Factory를 썼다는 것만이 다르다.

다시 그림으로 표현해보자.

그림으로 이해해보기

역할과 책임을 팩토리에게 위임한다.

 

댓글