SpringFramework์ ์ด๋ฒคํธ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ApplicationEventPublisher
๋ฅผ ์ ๊ณตํ๊ณ ์๋ค.
์ด๋ฌํ ApplicationEventPublisher
๋ฅผ ํ์ฉํด์ DDD์ ๋๋ฉ์ธ ์ด๋ฒคํธ๋ฅผ ์ฝ๊ฒ ์ฌ์ฉ ํ ์ ์๋ ์ฌ๋ฌ๊ฐ์ง ๋ฐฉ๋ฒ์ ์๋ํด๋ณด์๊ณ , ๊ฐ ๋ฐฉ๋ฒ์ ๋จ์ ์ ๋ํด์ ์์๋ณด๊ณ ์ ํ๋ค.
์ฒซ๋ฒ์งธ ๋ฐฉ๋ฒ์ 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๋ฅผ ๋ค๋ฅธ๊ณณ์์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ๋๋ฉ์ธ ์ด๋ฒคํธ ๋ฐํ์ด ๋๋ฝ ๋ ๊ฐ๋ฅ์ฑ์ด ๋์์ง๋๋ค.
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))
}
@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
๋ฅผ ํธ์ถํ์ง ์์ผ๋ฉด ๋๋ฉ์ธ ๋ก์ง์ ์คํ๋์ง๋ง ๋๋ฉ์ธ ์ด๋ฒคํธ๋ ๋ฐํ ๋์ง ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
์ 1,2๋ฒ์ ๋จ์ ์ ๋ณด์ํ๊ธฐ ์ํด์ AOP๋ฅผ ์ด์ฉํ ๋๋ฉ์ธ ์ด๋ฒคํธ ๋ฐํ ๋ฐฉ๋ฒ์ด ๋ง์ด ์ฌ์ฉ๋๊ณ ์์ต๋๋ค.
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
์ค์ ํ๋ก๋์
์์ ์ฌ์ฉํ์๋ 1๋ฒ๊ณผ 2๋ฒ์ ๋จ์ ์ ๋ณด์ํ๋ฉด์ ํธํ๊ฒ ์ฌ์ฉ ํ ์ ์๋ ์ข์ ๋ฐฉ๋ฒ ์ด์๋ค. ํ์ง๋ง ํน์ด์ผ์ด์ค๊ฐ ํ๋๋ ๋ฐ์ํ๋ฉด์ ํ๊ฐ์ง ์ด์๊ฐ ์์๋๋ฐ Events#raise๋ฅผ ํตํด์ ์ฆ์ ์ด๋ฒคํธ๊ฐ ๋ฐํ๋๋ค ๋ณด๋ ํด๋น Entity๊ฐ Persisted๋ Entity๊ฐ ์๋๋๋ผ๋ ์ด๋ฒคํธ๊ฐ ๋ฐํ
๋๋ ์ด์๊ฐ ๋ฐ์ํ๊ฒ ๋์๋ค.
๊ฐ์ธ์ ์ธ ์๊ฐ์ Spring Data์์ ์ ๊ณตํ๋ @DomainEvents
๊ฐ ์ด๋ฐ ์ผ์ด์ค๋๋ฌธ์ save๋ฅผ ๋ช
์์ ์ผ๋ก ํธ์ถํ๋ ๋ฐฉ๋ฒ์ผ๋ก Persisted๋ Entity์์ ๋ณด์ฅํ๋๊ฒ ๊ฐ์๋ค. ํ์ง๋ง 2๋ฒ์ ๋จ์ ์ด ๋๋ฌด ์ปค์ ์ค ์ฌ์ฉ์๋ ์ํ์ด ์์๊ธฐ๋๋ฌธ์ 3๋ฒ์ ์์ด๋์ด์ 2๊ฐ์ง ์์ด๋์ด๋ฅผ ๋ํ๋ค.
์ค์น๋ ์๊ฐ)
์ผ๋ฐ์ ์ธ ์ผ์ด์ค์์ PK Auto Creation์ ์ฌ์ฉํ๋ค๋ฉด ID๊ฐ null์ธ๊ฒฝ์ฐ๋ก ์ฒดํฌํด๋ ๋ฌด๋ฐฉ ํ ๊ฒ ๊ฐ๋ค.
์์ด๋์ด๋ 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<*>>>>()
}
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๋ฒ์ ๋ฐฉ์์ ๊ฐ๊ฐ์ ์ฅ,๋จ์ ์ด ์กด์ฌํ๊ธฐ๋๋ฌธ์ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ ํ์๋ค์ ์ฑํฅ์ ๋ง์ถฐ์ ์ทจ์ฌ ์ ํ์ ํ๋ฉด ๋ ๊ฒ ๊ฐ๋ค.