Tags

๐Ÿง‘โ€๐Ÿ’ป Spring์—์„œ DDD Domain Event ์‚ฌ์šฉํ•˜๊ธฐ

namjug-kim โ€ข 2020๋…„ 3์›” 24์ผ
DDD

์„œ๋ก 

SpringFramework์€ ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ApplicationEventPublisher๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ๋‹ค. ์ด๋Ÿฌํ•œ ApplicationEventPublisher๋ฅผ ํ™œ์šฉํ•ด์„œ DDD์˜ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ์‰ฝ๊ฒŒ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์‹œ๋„ํ•ด๋ณด์•˜๊ณ , ๊ฐ ๋ฐฉ๋ฒ•์˜ ๋‹จ์ ์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด๊ณ ์ž ํ•œ๋‹ค.

1. ๋ช…์‹œ์ ์œผ๋กœ ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ

์ฒซ๋ฒˆ์งธ ๋ฐฉ๋ฒ•์€ Spring Framework์˜ ApplicationEventPublisher๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ• ์ž…๋‹ˆ๋‹ค. ApplicationService์—์„œ ๋„๋ฉ”์ธ ๋กœ์ง์„ ์‹คํ–‰ํ•˜๊ณ  ๋„๋ฉ”์ธ ๋กœ์ง์— ํ•ด๋‹นํ•˜๋Š” DomainEvent๋ฅผ ์ง์ ‘ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

@Entity
class Match {
    @Id
    @GeneratedValue
    val id: Long? = null

    var homeScore: Int = 0
        private set

    var awayScore: Int = 0
        private set

    fun confirmMatchScore(homeScore: Int, awayScore: Int) {
        if (homeScore < 0 || awayScore < 0) {
            throw IllegalArgumentException("score value is negative.")
        }

        this.homeScore = homeScore
        this.awayScore = awayScore
    }
}
@Service
class ManualEventService(
    private val applicationEventPublisher: ApplicationEventPublisher,
    private val matchRepository: MatchRepository
) {
    @Transactional
    fun confirmMatchScore(matchId: Long, homeScore: Int, awayScore: Int) {
        val match = (matchRepository.findByIdOrNull(matchId)
            ?: throw IllegalArgumentException("match doesn't exists."))

        match.confirmMatchScore(homeScore, awayScore)
        // ๋ช…์‹œ์ ์ธ ์ด๋ฒคํŠธ ๋ฐœ์ƒ
        applicationEventPublisher.publishEvent(MatchConfirmedEvent(matchId, homeScore, awayScore))
    }
}

๋ณ„๋‹ค๋ฅธ ์„ค์ • ์—†์ด ๋„๋ฉ”์ธ ๋กœ์ง ์ดํ›„์— ๋กœ์ง์— ํ•ด๋‹นํ•˜๋Š” Event๋ฅผ ์„œ๋น„์Šค๋ ˆ์ด์–ด์—์„œ ๋ฐœ์ƒ ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ์—์„œ๋„ ์•Œ ์ˆ˜ ์žˆ๋“ฏ์ด, ์‹ค์ œ ๋„๋ฉ”์ธ ๋กœ์ง์€ Entity ๋‚ด๋ถ€์—์„œ ๋ฐœ์ƒํ•˜์ง€๋งŒ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋Š” ์„œ๋น„์Šค๋ ˆ์ด์–ด์—์„œ ๋ฐœํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ์˜ ์‘์ง‘๋„๊ฐ€ ๋–จ์–ด์ง€๊ณ  ์ด๋Ÿฌํ•œ ๋ฌธ๋งฅ์„ ์‚ฌ์ „์— ์•Œ์ง€ ๋ชปํ•˜๋Š” ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๊ฐ€ match#confirmMatchScore๋ฅผ ๋‹ค๋ฅธ๊ณณ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰์ด ๋ˆ„๋ฝ ๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„์ง‘๋‹ˆ๋‹ค.

2. Spring Framework @DomainEvents๋ฅผ ์ด์šฉํ•˜์—ฌ ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ

Spring Framework์—์„œ๋Š” ์ด๋Ÿฌํ•œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰์„ Entity class์—์„œ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋„๋ก @DomainEvents์™€ @AfterDomainEventPublication๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

@DomainEvents ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด์„œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ชฉ๋ก์„ ๋„˜๊ธฐ๊ณ  @AfterDomainEventPublication์€ @DomainEvents๋กœ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ์ „๋‹ฌํ•  ์ดํ›„์— ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

์ง์ ‘ @DomainEvents์™€ @AfterDomainEventPublication ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•œ abstract class๋ฅผ ๊ตฌํ˜„ํ•ด๋„ ๋˜๊ณ  Spring Data์—์„œ ์ œ๊ณตํ•˜๋Š” AbstractAggregateRoot๋ฅผ ์‚ฌ์šฉํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค.

/**
 * Clears all domain events currently held. Usually invoked by the infrastructure in place in Spring Data
 * repositories.
 */
@AfterDomainEventPublication
protected void clearDomainEvents() {
  this.domainEvents.clear();
}

/**
 * All domain events currently captured by the aggregate.
 */
@DomainEvents
protected Collection<Object> domainEvents() {
  return Collections.unmodifiableList(domainEvents);
}

AbstractAggregateRoot๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์„œ๋น„์Šค๋ ˆ์ด์–ด์—์„œ ๋ฐœํ–‰ํ•˜๋˜ ์ด๋ฒคํŠธ ๋กœ์ง์„ ์ œ๊ฑฐํ•˜๊ณ  ์‹ค์ œ ๋„๋ฉ”์ธ ๋กœ์ง์—์„œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

fun confirmMatchScore(homeScore: Int, awayScore: Int) {
    if (homeScore < 0 || awayScore < 0) {
        throw IllegalArgumentException("score value is negative.")
    }

    this.homeScore = homeScore
    this.awayScore = awayScore
    this.registerEvent(MatchConfirmedEvent(this.id ?: error("not persisted"), homeScore, awayScore))
}

2.1. @DomainEvents Under the Hood

@DomainEvents๋Š” RepositoryProxyPostProcessor๊ตฌํ˜„์ฒด์ธ EventPublishingRepositoryProxyPostProcessor๋ฅผ ํ†ตํ•ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

/*
 * (non-Javadoc)
 * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
 */
@Override
public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {

	Object[] arguments = invocation.getArguments();
	Object result = invocation.proceed();

	if (!invocation.getMethod().getName().startsWith("save")) {
		return result;
	}

	Object eventSource = arguments.length == 1 ? arguments[0] : result;

	eventMethod.publishEventsFrom(eventSource, publisher);

	return result;
}

EventPublishingRepositoryProxyPostProcessor๋Š” RepositoryFactoryBeanSupport๋ฅผ ํ†ตํ•ด์„œ ๋“ฑ๋ก๋˜๋ฉฐ ์œ„ ์ฝ”๋“œ์™€ ๊ฐ™์ด Spring Data Repository์˜ save~๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ฉ”์†Œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ์„๋•Œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. Method Interceptor๋ฅผ ํ†ตํ•ด์„œ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— Entityํด๋ž˜์Šค์—์„œ ApplicationEventPublisher bean์„ ๊ฐ€์ง€๊ณ  ์žˆ์ง€ ์•Š๋”๋ผ๋„ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๊ธฐ๋•Œ๋ฌธ์— ๋„๋ฉ”์ธ ๋กœ์ง์—์„œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ ํ•œ๊ฐ€์ง€ ๋ฌธ์ œ์ ์ด ์ƒ๊ธฐ๊ฒŒ ๋˜๋Š”๋ฐ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๊ฐ€ Method Interceptor๋ฅผ ํ†ตํ•ด ๋ฐœํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— Repository์˜ save๋ฉ”์†Œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜์–ด์•ผ์ง€๋งŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋กœ์ง์ด ์‹คํ–‰๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. JPA์˜ ๋ณ€๊ฒฝ ๊ฐ์ง€(Dirty Checking)์™€ ๊ฐ™์ด save ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ์—†์ด ๋ณ€๊ฒฝ๋™์ž‘์ด ๊ฐ€๋Šฅํ•˜๊ธฐ๋•Œ๋ฌธ์— ์ด๋Ÿฌํ•œ ๋ฌธ๋งฅ์„ ์ด์–ด๊ฐ€์ง€ ์•Š๊ณ  ๋„๋ฉ”์ธ ๋กœ์ง์„ ์‹คํ–‰ํ•œ ์ดํ›„์— Repository#save๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์œผ๋ฉด ๋„๋ฉ”์ธ ๋กœ์ง์€ ์‹คํ–‰๋˜์ง€๋งŒ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋Š” ๋ฐœํ–‰ ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

3. AOP๋ฅผ ์ด์šฉํ•œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰

์œ„ 1,2๋ฒˆ์˜ ๋‹จ์ ์„ ๋ณด์™„ํ•˜๊ธฐ ์œ„ํ•ด์„œ AOP๋ฅผ ์ด์šฉํ•œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋ฐฉ๋ฒ•์ด ๋งŽ์ด ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

  • 1๋ฒˆ์˜ ์„œ๋น„์Šค๋ ˆ์ด์–ด์—์„œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•ด ๋„๋ฉ”์ธ ๋กœ์ง ์‹คํ–‰๊ณผ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰์˜ ์‘์ง‘๋„๊ฐ€ ๋–จ์–ด์ง„๋‹ค.
  • 2๋ฒˆ์˜ save๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœํ•ด์•ผ์ง€๋งŒ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋„๋ฉ”์ธ ๋กœ์ง์ด ๋ณ€๊ฒฝ๊ฐ์ง€๋กœ ์™„๋ฃŒ๋˜์—ˆ์„๋•Œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰์ด ๋ˆ„๋ฝ๋˜๋Š” ์ผ€์ด์Šค๊ฐ€ ๋ฐœํ–‰ํ•œ๋‹ค.

ApplicationEventPublisher๋ฅผ ThreadLocal์— ๋ณด๊ด€ํ•˜๋Š” ์ปจํ…์ŠคํŠธ ํด๋ž˜์Šค

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

    public static void raise(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();
    }
}
@Aspect
@Component
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 = false;
        
        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;
    }
}

๋„๋ฉ”์ธ ๋กœ์ง์€ ApplicationService(@Service + @Transactional)์—์„œ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— @Transactional pointcut์„ ํ†ตํ•ด์„œ ์„œ๋น„์Šค๋ ˆ์ด์–ด ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ์‹œ์ ์— ThreadLocal์„ ํ†ตํ•ด์„œ Spring์˜ ApplicationEventPublisher์„ ๋„˜๊ฒจ์ค˜์„œ Entity์—์„œ Events#raise๋ฅผ ํ˜ธ์ถœํ•จ์œผ๋กœ์จ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค. e

3.1. AOP์™€ ThreadLocal์„ ์ด์šฉํ•œ ์ด๋ฒคํŠธ ๋ฐœํ–‰์˜ ๋ฌธ์ œ์ 

์‹ค์ œ ํ”„๋กœ๋•์…˜์—์„œ ์‚ฌ์šฉํ–ˆ์„๋•Œ 1๋ฒˆ๊ณผ 2๋ฒˆ์˜ ๋‹จ์ ์„ ๋ณด์™„ํ•˜๋ฉด์„œ ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ๋ฐฉ๋ฒ• ์ด์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ํŠน์ด์ผ€์ด์Šค๊ฐ€ ํ•˜๋‚˜๋‘˜ ๋ฐœ์ƒํ•˜๋ฉด์„œ ํ•œ๊ฐ€์ง€ ์ด์Šˆ๊ฐ€ ์žˆ์—ˆ๋Š”๋ฐ Events#raise๋ฅผ ํ†ตํ•ด์„œ ์ฆ‰์‹œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋˜๋‹ค ๋ณด๋‹ˆ ํ•ด๋‹น Entity๊ฐ€ Persisted๋œ Entity๊ฐ€ ์•„๋‹ˆ๋”๋ผ๋„ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋˜๋Š” ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. ๊ฐœ์ธ์ ์ธ ์ƒ๊ฐ์— Spring Data์—์„œ ์ œ๊ณตํ•˜๋Š” @DomainEvents๊ฐ€ ์ด๋Ÿฐ ์ผ€์ด์Šค๋•Œ๋ฌธ์— save๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ Persisted๋œ Entity์ž„์„ ๋ณด์žฅํ•˜๋Š”๊ฒƒ ๊ฐ™์•˜๋‹ค. ํ•˜์ง€๋งŒ 2๋ฒˆ์˜ ๋‹จ์ ์ด ๋„ˆ๋ฌด ์ปค์„œ ์‹ค ์‚ฌ์šฉ์—๋Š” ์œ„ํ—˜์ด ์žˆ์—ˆ๊ธฐ๋•Œ๋ฌธ์— 3๋ฒˆ์˜ ์•„์ด๋””์–ด์— 2๊ฐ€์ง€ ์•„์ด๋””์–ด๋ฅผ ๋”ํ–ˆ๋‹ค.

  • Hibernate Session์—์„œ ๊ด€๋ฆฌ์ค‘์ธ Entity๋Š” ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ์ฆ‰์‹œ ๋ฐœํ–‰ํ•œ๋‹ค.
  • Hibernate Session์—์„œ ๊ด€๋ฆฌ์ค‘์ด์ง€ ์•Š์€ Entity์—์„œ ๋ฐœํ–‰๋œ ์ด๋ฒคํŠธ๋Š” Queue์— ์ €์žฅํ•ด ๋‘์—ˆ๋‹ค๊ฐ€ save ํ˜ธ์ถœ ์‹œ์ ์— ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•œ๋‹ค.
์Šค์น˜๋Š” ์ƒ๊ฐ)
์ผ๋ฐ˜์ ์ธ ์ผ€์ด์Šค์—์„œ PK Auto Creation์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ID๊ฐ€ null์ธ๊ฒฝ์šฐ๋กœ ์ฒดํฌํ•ด๋„ ๋ฌด๋ฐฉ ํ•  ๊ฒƒ ๊ฐ™๋‹ค.

4. Sessionํ™•์ธ์„ ์ ์šฉํ•œ AOP

์•„์ด๋””์–ด๋Š” 3๋ฒˆ์˜ ์•„์ด๋””์–ด์™€ ๊ฑฐ์˜ ๋™์ผํ•˜๋ฉฐ ๋ช‡๊ฐ€์ง€ ์•„์ด๋””์–ด๋ฅผ ๋”ํ•˜์˜€๋‹ค.

object Events {
    private val publisherLocal = ThreadLocal<ApplicationEventPublisher>()
    private val entityManagerLocal = ThreadLocal<EntityManager>()
    private val publishedEventLocal = ThreadLocal<HashSet<UUID>>()

    /**
     * session ์—์„œ ๊ด€๋ฆฌ์ค‘์ด์ง€ ์•Š์€ Entity์—์„œ ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ๋ฅผ ๋‹ด์•„๋‘”๋‹ค.
     */
    private val waitingEventQueue = ThreadLocal<MutableMap<Int, Queue<DomainEventHolder<*>>>>()
}
  1. EntityManager๋ฅผ ๋„˜๊ฒจ์ฃผ์–ด Session์—์„œ ๊ด€๋ฆฌ์ค‘์ธ ์ƒํƒœ๋ฅผ ํ™•์ธํ•œ๋‹ค.
  2. publishedEventLocal(HashSet)์„ ํ†ตํ•ด์„œ ์ด๋ฒคํŠธ๊ฐ€ ์ค‘๋ณต ๋ฐœํ–‰๋˜์ง€ ์•Š๋„๋ก ํ•œ๋‹ค.
  3. waitingEventQueue์— Session์—์„œ ๊ด€๋ฆฌ์ค‘์ด์ง€ ์•Š์€ Entity์—์„œ ๋ฐœํ–‰๋œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ์ €์žฅํ•˜๊ณ  Repository#save~ ํ˜ธ์ถœ์‹œ์ ์— ๋ฐœํ–‰ํ•œ๋‹ค.
/**
 * session์—์„œ ๊ด€๋ฆฌ์ค‘์ธ [DomainEntity]์—์„œ ๋ฐœ์ƒํ•œ [DomainEvent] ๋Š” ์ฆ‰์‹œ ๋ฐœํ–‰ํ•œ๋‹ค.
 * session์—์„œ ๊ด€๋ฆฌ์ค‘์ด์ง€ ์•Š์€ [DomainEntity]์—์„œ ๋ฐœ์ƒํ•œ [DomainEvent]๋Š” [waitingEventQueue]์— ๋‹ด์•„๋‘์—ˆ๋‹ค๊ฐ€ [DomainEntity]๊ฐ€ save๋ ๋•Œ ๋ฐœํ–‰ํ•œ๋‹ค.
 */
fun <T> publish(entity: DomainEntity<T>, event: DomainEvent<T>) {
    if (entityManagerLocal.get().contains(entity)) {
        this.publishEvent(entity, event)
    } else {
        val entityDomainEventQueue = waitingEventQueue.get()
        val entityClassHashCode = System.identityHashCode(entity)
        val queue = entityDomainEventQueue.computeIfAbsent(entityClassHashCode) { LinkedList() }
        queue.add(DomainEventHolder(entity, event))
    }
}

๋™์ž‘์„ ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด๋ฉด Events.publish(/*Domain*/, /*DomainEvent*/)๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰์‹œ ๋„๋ฉ”์ธ๊ณผ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ชจ๋‘ ๋„˜๊ฒจ๋ฐ›๊ณ 

  • ๋„๋ฉ”์ธ์ด ์„ธ์…˜์—์„œ ๊ด€๋ฆฌ์ค‘์ธ์ง€ ํ™•์ธํ•œ๋‹ค.
  • ์„ธ์…˜์—์„œ ๊ด€๋ฆฌ์ค‘์ด๋ผ๋ฉด ์ฆ‰์‹œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•œ๋‹ค.
  • ์„ธ์…˜์—์„œ ๊ด€๋ฆฌ์ค‘์ด ์•„๋‹ˆ๋ผ๋ฉด ๋Œ€๊ธฐ ํ์— ์ถ”๊ฐ€ํ•œ๋‹ค.

์—ฌ๊ธฐ์„œ ์กฐ๊ธˆ ๋…ํŠนํ•˜๊ฒŒ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด์„œ val entityClassHashCode = System.identityHashCode(entity)๋ฅผ key๊ฐ’์œผ๋กœ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ๋„๋ฉ”์ธ ํด๋ž˜์Šค๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ PK๋ฅผ ํ†ตํ•œ equals๋ฅผ ๊ตฌํ˜„ํ•˜๋Š”๋ฐ ์„ธ์…˜์—์„œ ๊ด€๋ฆฌ์ค‘์ด์ง€ ์•Š์€ Entity์˜ ๊ฒฝ์šฐ PK๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๊ธฐ๋•Œ๋ฌธ์— key๊ฐ’์„ ์œ„ํ•œ hashCode๋ฅผ ๋ณ„๋„๋กœ ์ƒ์„ฑํ•˜์˜€๋‹ค.

/**
 * session์—์„œ ๊ด€๋ฆฌ๋˜์ง€ ์•Š๋Š” ์ƒํƒœ์˜ Entity์—์„œ ์ƒ์„ฑ๋˜์—ˆ๋˜ [DomainEvent]๋ฅผ ๋ฐœํ–‰ํ•œ๋‹ค.
 */
fun publishWaitingQueue(entityClass: Any) {
    if (waitingEventQueue.get() == null) {
        return
    }

    val entityDomainEventQueue = waitingEventQueue.get()
    val entityClassHashCode = System.identityHashCode(entityClass)
    entityDomainEventQueue[entityClassHashCode]?.pollAll {
        val domainEventHolder = it as DomainEventHolder<Any>
        domainEventHolder.domainEvent.aggregateRootId = domainEventHolder.domainEntity.getId()
        this.publishEvent(domainEventHolder.domainEntity, domainEventHolder.domainEvent)
    }
}

๋Œ€๊ธฐํ์— ์ €์žฅ๋œ ๋ฐœํ–‰๋œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰์‹œํ‚ค๊ธฐ ์œ„ํ•ด์„œ 2๋ฒˆ์˜ RepositoryProxyPostProcessor๋ฅผ ์‚ฌ์šฉํ•˜์˜€๋‹ค.

class CustomJpaRepositoryFactoryBean<T : Repository<S, ID>, S, ID>(repositoryInterface: Class<out T>) : JpaRepositoryFactoryBean<T, S, ID>(repositoryInterface) {
    override fun doCreateRepositoryFactory(): RepositoryFactorySupport {
        val repositoryFactorySupport = super.doCreateRepositoryFactory()
        repositoryFactorySupport.addRepositoryProxyPostProcessor { factory: ProxyFactory, _: RepositoryInformation? ->
            factory.addAdvice(MethodInterceptor { invocation: MethodInvocation ->
                val arguments = invocation.arguments
                val result = invocation.proceed()
                if (!invocation.method.name.startsWith("save")) {
                    return@MethodInterceptor result
                }

                val eventSource = if (arguments.size == 1) arguments[0] else result
                if (eventSource is List<*>) {
                    eventSource.asSequence()
                        .filterNotNull()
                        .forEach { publishWaitingQueue(it) }
                } else {
                    publishWaitingQueue(eventSource)
                }
                result
            })
        }
        return repositoryFactorySupport
    }
}

Repository ์ƒ์„ฑ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์„ ์œ„ํ•ด์„œ JpaRepositoryFactoryBean๋ฅผ ์ƒ์†๋ฐ›์•„ doCreateRepositoryFactory๋ฉ”์†Œ๋“œ์—์„œ Repository#save๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ์‹œ์ ์— Events.publishWaitingQueue๋ฅผ ์‹คํ–‰ํ•˜์˜€๋‹ค.

์ดํ‰

์„œ๋น„์Šค๋ ˆ์ด์–ด์—์„œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š”๊ฒƒ๋ถ€ํ„ฐ AOP๋ฅผ ์ด์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๊นŒ์ง€ ์ด 4๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด ๋ณด์•˜๋‹ค. ์ฒ˜์Œ์—๋Š” Spring Data์—์„œ ์ œ๊ณต๋˜๋Š” 2๋ฒˆ์˜ ๋ฐฉ๋ฒ•(Repository#save๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœ)์ด ์ž˜ ๋‚ฉ๋“์ด ๋˜์ง€ ์•Š์•˜๋Š”๋ฐ 3,4๋ฒˆ์˜ ๊ณผ์ •์„ ๊ฑฐ์น˜๋ฉด์„œ ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์ผ€์ด์Šค๋ฅผ ๋งˆ์ฃผํ•˜๋‹ค ๋ณด๋‹ˆ ์™œ ์ €๋ ‡๊ฒŒ ์ œ๊ณต ๋˜์—ˆ๋Š”์ง€ ๋‚ฉ๋“์ด ๋˜์—ˆ๋‹ค. ๋‹ค์Œ๋ถ€ํ„ฐ๋Š” Spring Framework(๊ผญ Spring์ด ์•„๋‹ˆ๋”๋ผ๋„ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” ์˜คํ”ˆ์†Œ์Šค ํ”„๋ ˆ์ž„์›Œํฌ)์ด ์ œ๊ณตํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์กฐ๊ธˆ๋” ๊นŠ๊ฒŒ ์ดํ•ดํ•˜๊ณ  ์ ‘๊ทผํ•˜๋Š” ์‹œ๋„๊ฐ€ ํ•„์š”ํ•˜๊ฒ ๋‹ค ๋ผ๋Š” ์ƒ๊ฐ์„ ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. ๊ฐœ์ธ์ ์œผ๋กœ 1๋ฒˆ์„ ์ œ์™ธํ•œ 2,3,4๋ฒˆ์˜ ๋ฐฉ์‹์€ ๊ฐ๊ฐ์˜ ์žฅ,๋‹จ์ ์ด ์กด์žฌํ•˜๊ธฐ๋•Œ๋ฌธ์— ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ํŒ€์›๋“ค์— ์„ฑํ–ฅ์— ๋งž์ถฐ์„œ ์ทจ์‚ฌ ์„ ํƒ์„ ํ•˜๋ฉด ๋  ๊ฒƒ ๊ฐ™๋‹ค.

reference

  • https://www.baeldung.com/spring-data-ddd
  • https://supawer0728.github.io/2018/03/24/spring-event/
  • https://javacan.tistory.com/entry/Handle-DomainEvent-with-Spring-ApplicationEventPublisher-EventListener-TransactionalEventListener