Skip to content

Commit

Permalink
[Feat/#508] 도메인 이벤트 적용 (#509)
Browse files Browse the repository at this point in the history
* feat: 도메인 이벤트 구현

* refactor: 도메인 이벤트 구현
  • Loading branch information
belljun3395 authored Jan 8, 2025
1 parent 5369cd1 commit 0d16ab8
Show file tree
Hide file tree
Showing 13 changed files with 392 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package com.few.crm.email.domain

import com.few.crm.email.event.template.PostEmailTemplateEvent
import com.few.crm.support.jpa.converter.StringListConverter
import jakarta.persistence.*
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.domain.AbstractAggregateRoot
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDateTime

@Entity
@Table(name = "email_templates")
@EntityListeners(AuditingEntityListener::class)
data class EmailTemplate(
class EmailTemplate(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
Expand All @@ -27,7 +29,7 @@ data class EmailTemplate(
var version: Float = 1.0f,
@CreatedDate
var createdAt: LocalDateTime? = null,
) {
) : AbstractAggregateRoot<EmailTemplate>() {
protected constructor() : this(
templateName = "",
subject = "",
Expand All @@ -50,7 +52,15 @@ data class EmailTemplate(
)
}

fun isNewTemplate(): Boolean = id == null
fun isNewTemplate(): Boolean {
registerEvent(
PostEmailTemplateEvent(
templateId = this.id ?: 0,
eventType = "POST",
),
)
return id == null
}

fun modifySubject(subject: String?): EmailTemplate {
subject?.let {
Expand All @@ -77,6 +87,13 @@ data class EmailTemplate(
} ?: kotlin.run {
this.version += 0.1f
}

registerEvent(
PostEmailTemplateEvent(
templateId = this.id!!,
eventType = "POST",
),
)
return this
}
}
24 changes: 24 additions & 0 deletions domain/crm/src/main/kotlin/com/few/crm/email/domain/SentEmail.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.few.crm.email.domain

import com.few.crm.email.event.send.EmailSentEvent
import org.springframework.data.domain.AbstractAggregateRoot

class SentEmail(
private val userExternalId: String,
private val emailBody: String,
private val destination: String,
private val emailMessageId: String,
private val eventType: EmailSendEventType = EmailSendEventType.SEND,
) : AbstractAggregateRoot<SentEmail>() {
init {
registerEvent(
EmailSentEvent(
userExternalId = userExternalId,
emailBody = emailBody,
messageId = emailMessageId,
destination = destination,
eventType = eventType.name,
),
)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.few.crm.email.service

import com.few.crm.email.domain.EmailSendEventType
import com.few.crm.email.domain.SentEmail
import email.*
import email.provider.CrmAwsSESEmailSendProvider
import event.domain.PublishEvents
import org.springframework.boot.autoconfigure.mail.MailProperties
import org.springframework.stereotype.Component

Expand All @@ -13,6 +16,20 @@ data class SendEmailArgs(
override val properties: String = "",
) : SendMailArgs<NonContent, String>

data class SendEmailDto(
val to: String,
val subject: String,
val template: String,
val content: NonContent,
val properties: String = "",
val userExternalId: String,
val emailBody: String,
val destination: String,
val eventType: EmailSendEventType,
) {
val emailArgs = SendEmailArgs(to, subject, template, content, properties)
}

@Component
class CrmSendNonVariablesEmailService(
mailProperties: MailProperties,
Expand All @@ -23,6 +40,18 @@ class CrmSendNonVariablesEmailService(
val context = EmailContext()
return emailTemplateProcessor.process(args.template, context, EmailTemplateType.STRING)
}

@PublishEvents
fun send(args: SendEmailDto): SentEmail {
val emailArgs = args.emailArgs
return SentEmail(
userExternalId = args.userExternalId,
emailBody = args.emailBody,
destination = args.destination,
emailMessageId = send(emailArgs),
eventType = args.eventType,
)
}
}

class NonContent
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
package com.few.crm.email.usecase

import com.few.crm.email.domain.EmailTemplate
import com.few.crm.email.event.template.PostEmailTemplateEvent
import com.few.crm.email.repository.EmailTemplateRepository
import com.few.crm.email.service.HtmlValidator
import com.few.crm.email.usecase.dto.PostTemplateUseCaseIn
import com.few.crm.email.usecase.dto.PostTemplateUseCaseOut
import com.few.crm.support.jpa.CrmTransactional
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service

@Service
class PostTemplateUseCase(
private val emailTemplateRepository: EmailTemplateRepository,
private val applicationEventPublisher: ApplicationEventPublisher,
private val htmlValidator: HtmlValidator,
) {
@CrmTransactional
Expand Down Expand Up @@ -64,13 +61,6 @@ class PostTemplateUseCase(
}
}

applicationEventPublisher.publishEvent(
PostEmailTemplateEvent(
templateId = modifiedOrNewTemplate.id!!,
eventType = "POST",
),
)

return run {
PostTemplateUseCaseOut(
id = modifiedOrNewTemplate.id!!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ package com.few.crm.email.usecase

import com.fasterxml.jackson.databind.ObjectMapper
import com.few.crm.email.domain.EmailSendEventType
import com.few.crm.email.event.send.EmailSentEvent
import com.few.crm.email.repository.EmailTemplateHistoryRepository
import com.few.crm.email.repository.EmailTemplateRepository
import com.few.crm.email.service.CrmSendNonVariablesEmailService
import com.few.crm.email.service.NonContent
import com.few.crm.email.service.SendEmailArgs
import com.few.crm.email.service.SendEmailDto
import com.few.crm.email.usecase.dto.SendNotificationEmailUseCaseIn
import com.few.crm.email.usecase.dto.SendNotificationEmailUseCaseOut
import com.few.crm.user.repository.UserRepository
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service

data class NotificationEmailTemplateProperties(
Expand All @@ -25,7 +23,6 @@ class SendNotificationEmailUseCase(
private val emailTemplateHistoryRepository: EmailTemplateHistoryRepository,
private val userRepository: UserRepository,
private val crmSendNonVariablesEmailService: CrmSendNonVariablesEmailService,
private val applicationEventPublisher: ApplicationEventPublisher,
private val objectMapper: ObjectMapper,
) {
fun execute(useCaseIn: SendNotificationEmailUseCaseIn): SendNotificationEmailUseCaseOut {
Expand All @@ -42,23 +39,16 @@ class SendNotificationEmailUseCase(
}

targetUsers.keys.forEach { email ->
val emailMessageId =
crmSendNonVariablesEmailService.send(
SendEmailArgs(
to = email,
subject = properties.subject,
template = properties.body,
content = NonContent(),
),
)

applicationEventPublisher.publishEvent(
EmailSentEvent(
crmSendNonVariablesEmailService.send(
SendEmailDto(
to = email,
subject = properties.subject,
template = properties.body,
content = NonContent(),
userExternalId = targetUsers[email]!!.first().externalId!!,
emailBody = properties.body,
destination = email,
messageId = emailMessageId,
eventType = EmailSendEventType.SEND.name,
eventType = EmailSendEventType.SEND,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package event.domain

import org.springframework.aot.hint.annotation.Reflective

@Reflective
@Retention(AnnotationRetention.RUNTIME)
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER,
AnnotationTarget.ANNOTATION_CLASS,
)
annotation class AfterEventPublication
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package event.domain

/**
* Domain abstract aggregate root
*
* @see org.springframework.data.domain.AbstractAggregateRoot
*/
abstract class DomainAbstractAggregateRoot<A : DomainAbstractAggregateRoot<A>> {
@Transient
private val domainEvents: MutableList<Any> = mutableListOf()

protected fun <T> registerEvent(event: T): T {
requireNotNull(event) { "Domain event must not be null" }
domainEvents.add(event)
return event
}

@AfterEventPublication
protected fun clearDomainEvents() {
domainEvents.clear()
}

@DomainEvents
protected fun domainEvents(): List<Any> = domainEvents.toList()

protected fun andEventsFrom(aggregate: A): A {
domainEvents.addAll(aggregate.domainEvents())
@Suppress("UNCHECKED_CAST")
return this as A
}

protected fun andEvent(event: Any): A {
registerEvent(event)
@Suppress("UNCHECKED_CAST")
return this as A
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package event.domain

import event.domain.DomainEventPublishingMethod.Companion.NONE
import event.domain.util.AnnotationDetectionMethodCallback
import org.springframework.context.ApplicationEventPublisher
import org.springframework.lang.Nullable
import org.springframework.util.ReflectionUtils
import java.lang.reflect.Method
import java.util.function.Supplier

class DomainEventPublishingMethod(
private val type: Class<*>,
private val publishingMethod: Method?,
private val clearingMethod: Method? = null,
) {
companion object {
val NONE = DomainEventPublishingMethod(Any::class.java, null, null)

fun of(type: Class<*>): DomainEventPublishingMethod {
if (!type.superclass.isAssignableFrom(DomainAbstractAggregateRoot::class.java)) {
throw IllegalArgumentException("Type must extend DomainAbstractAggregateRoot")
}

val result =
from(
type,
getDetector(type, DomainEvents::class.java),
Supplier {
getDetector(
type,
AfterEventPublication::class.java,
)
},
)
return result
}
}

fun publishEventsFrom(
aggregates: Iterable<*>,
publisher: ApplicationEventPublisher,
) {
for (aggregateRoot in aggregates) {
if (!type.isInstance(aggregateRoot)) {
continue
}

for (event in asCollection(ReflectionUtils.invokeMethod(publishingMethod!!, aggregateRoot), null)) {
publisher.publishEvent(event)
}

if (clearingMethod != null) {
ReflectionUtils.invokeMethod(clearingMethod, aggregateRoot)
}
}
}
}

private fun from(
type: Class<*>,
publishing: AnnotationDetectionMethodCallback<*>,
clearing: Supplier<AnnotationDetectionMethodCallback<*>>,
): DomainEventPublishingMethod {
if (!publishing.hasFoundAnnotation()) {
return NONE
}

val eventMethod = publishing.getMethod()!!
ReflectionUtils.makeAccessible(eventMethod)

return DomainEventPublishingMethod(
type,
eventMethod,
getClearingMethod(clearing.get()),
)
}

@Nullable
private fun getClearingMethod(clearing: AnnotationDetectionMethodCallback<*>): Method? {
if (!clearing.hasFoundAnnotation()) {
return null
}

val method = clearing.getRequiredMethod()
ReflectionUtils.makeAccessible(method)

return method
}

private fun <T : Annotation> getDetector(
type: Class<*>,
annotation: Class<T>,
): AnnotationDetectionMethodCallback<T> {
val callback = AnnotationDetectionMethodCallback(annotation)
ReflectionUtils.doWithMethods(type, callback)

return callback
}
Loading

0 comments on commit 0d16ab8

Please sign in to comment.