OutBox Pattern μ νμ©ν λ©μΌ μ μ‘ μλΉμ€ κ°λ° [At Least Once]
μ€νλ§μμ λ©μΌ μ μ‘μ μ λ§ κ°λ¨νκ² κ΅¬νν μ μλ€. JavaMailSender λ‘ λ§μ΄μΌ.
λ¨μν, λ©μΌλ§ μ μ‘νλ ν¨μλ§ κ΅¬ννλ©΄ λμΌκΉ?
λ¬Όλ‘ μν©μ λ°λΌ κ°λ¨ν ꡬνμ΄λ, 볡μ‘ν ꡬνμ΄ λλ μ ΈμΌ νλ€.
μ€ μλΉμ€μμλ?
λ©μΌλ‘ μ μ‘ν΄μΌ νλ λ°μ΄ν° μ€μλμ λ°λΌ λ€λ₯΄κ² μ§λ§,
μ무λλ λ©μΌμ μμ νλ ν΄λΌμ΄μΈνΈ μ μ₯μμλ μλ² μ₯μ λλ¬Έμ, λ©μΌ μμ μ΄ μ λ κ²½μ° λ§€μ° λΉν©μ€λ¬μΈ κ²μ΄λ€.
μ¦, μ μ΄λ νλ² μ μ‘ (At least once) λ₯Ό λ§μ‘±νλ Eventually Consistency λ₯Ό ꡬνν΄μΌ νλ κ²μ λ©μΌ μ μ‘ μλΉμ€μμ κΈ°λ³Έμ μΌλ‘ λ€λ€μ ΈμΌ ν μ¬νμ΄λ€.
μΌλ‘λ‘, λΆμ° μλ² νκ²½μμλ, μλ¦Ό μλΉμ€λ§μ λ€λ£¨λ μ ν리μΌμ΄μ μ΄ μ‘΄μ¬νλλ°, μ΄λ outbox ν¨ν΄μ΄λΌλ κ²μ μ¬μ©νμ¬ κ΅¬ννλ€.
λ΄κ° κ°λ°νλ μλ²λ λͺ¨λ Έλ¦¬μ€ νκ²½μ΄λ―λ‘, out box ν¨ν΄κΉμ§λ μ¬μ©ν μ΄μ κ° μμ§λ§, μΈλΆ API λ₯Ό ν΅ν΄ λ°μ΄ν°λ₯Ό μ 곡λ°λ λ‘μ§μ΄ λ΄κ²¨ μκΈ°μ, out box ν¨ν΄κ³Ό λΉμ€λ¬΄λ¦¬νκ² κ΅¬νν΄λ΄€λ€.
At Least Once κ° λλ°?
μΌλ°μ μΌλ‘, λ°μ΄ν° μ μ‘ λ°©μμ μ΄ 3κ°μ§κ° μλ€.
At Least Once
- μ μ΄λ νλ² λ°μ΄ν° μ μ‘μ 보μ₯
- μ€λ³΅ λ°μ΄ν° μ μ‘ λ°μ μ΄μκ° μλ€. (μλμ μΌλ‘ κ°λ²Όμ΄ λ©μμ§μ λν΄μ μ μ©νλ©΄ μ’μ κ² κ°λ€.)
At Most Once
- νλ²μ λ°μ΄ν° μ μ‘μ 보μ₯
- λ°μ΄ν°μ μμ μκ° λ°μ΄ν°λ₯Ό μμ ν μ¬λΆλ μ€μνμ§ μλ€.
- λ°μ΄ν°μ μ μ€μ΄ λ°μν μ μλ€.
Exactly Once
- μ νν ν λ² λ°μ΄ν° μ μ‘
- Message Filtering μ νλ λ―Έλ€μ¨μ΄κ° 보μ₯λμ΄μΌ μ νν ν λ² λ°μ΄ν° μ μ‘ κ°λ₯
- ꡬννκΈ° μ°Έ μ΄λ ΅λ€.
OutBox ν¨ν΄μ΄λ?
MSA νκ²½μμ μ£Όλ‘ μ¬μ©νλ ν¨ν΄
Message Queue μ κ°λ
κ³Ό μ μ¬νλ€.
λΆμ° μ ν리μΌμ΄μ
κ°μ λ°μ΄ν° μ μ‘ μ λ°μ΄ν°μ μΌκ΄μ±μ μ μ§ν μ μλ€.
λ°μ΄ν°μ μμ μ¬νμ΄ μκΈΈ κ²½μ°, μ΄λ²€νΈλ₯Ό λ°μ μμΌ μ΄λ²€νΈμ λ΄μ©μ OutBox μ μ μ₯νλ€.
OutBox λ₯Ό ꡬλ
νλ Message Relay κ° μ μ₯λ μ΄λ²€νΈλ₯Ό μ²λ¦¬νλ λ°©μ
λλ μ΄λ€ μν©μ΄μμκΉ?
μΌλ¨ μΈλΆ API λ‘λΆν° λ°μ΄ν°λ₯Ό Fetching ν΄μμΌ νλ€.
κ·Έ λ°μ΄ν°λ₯Ό λ°λ‘ λ³΄λΌ μ μμ§λ§, μΈμ μ΄λμλ κ³ λ €ν΄μΌ νλ μμΈ μν©!
κ·Έλμ λλ λ°μ΄ν°λ₯Ό κ°μ Έμ€λ κ²κ³Ό, κ·Έ λ°μ΄ν°λ₯Ό κ°μ§κ³ λ©μΌ μ μ‘νλ λ‘μ§μ λλ κ°λ°ν΄μΌνλ€.
κ·Έλμ λ°μ΄ν°λ₯Ό μμλ‘ μ μ₯ν΄ λκ³ μμ΄μΌ νλ€.
λ°μ΄ν°λ₯Ό fetching ν΄, out box λ₯Ό μ‘°κΈ κ°μν mongodb document μ μ μ₯νλ μ€μΌμ₯΄λ¬,
mongodb μ μ μ₯λ λ°μ΄ν°λ₯Ό κ°κ³΅ν΄ μ μ΄λ νλ² μ μ‘μ 보μ₯νλ λ©μΌ μ μ‘ μ€μΌμ₯΄λ¬
λ΄κ° κ°λ°ν λͺ©λ‘μ΄λ€.
ꡬν κ³Όμ
1. λ°μ΄ν°λ₯Ό API λ‘λΆν° νμΉν΄, out box (mail box) document μ μ μ₯
λλ Google Analytics4 μ λ°μ΄ν°λ₯Ό νμΉν΄μμΌ νλ€.
κ·Έ λ°μ΄ν°λ₯Ό νμΉν΄ μ μ₯ν document μΈ mail box
@Document(collection = "mail_box")
class MailBox(
@Id
@Field(name = "_id", targetType = FieldType.OBJECT_ID)
val id: ObjectId = ObjectId.get(),
var event: Event,
var payload: Payload,
)
data class Event(
@Field(name = "event_type")
var eventType: String,
@Field(name = "period")
var period: String,
)
enum class ExtractPeriod {
WEEK,
MONTH,
}
data class Payload(
val payload: String,
)
μ€μ λ‘ out box λͺ¨λΈμ μ΄μ λ€λ₯΄μ§λ§, λλ μ°λ¦¬ μλΉμ€μ νμν νλλ§μΌλ‘ κ°μνμ¬ κ΅¬μ±νμλ€.
μ£Όμ μ λ¨μλ‘ μ μ‘μ νμΈνκΈ° μν΄, enum νμ
μ μ μνμκ³ ,
μ΄λ²€νΈ νμ
μΌλ‘ μ΄λ ν λ°μ΄ν° μ μ‘ νμ
μΈμ§ λͺ
μνμκ³ ,
μ€μ§μ μΌλ‘ μ μ‘ν λ°μ΄ν°μ μμΈ μ¬νμ payload μ json νμμΌλ‘ λ³ννμ¬ μ μ₯νλ€.
@Service
class ExampleGA4InfoBoxService(
private val exampleBlangcoGA4Service: GA4Service,
private val exampleMailBoxBuilder: MailBoxBuilder<ExampleGA4Data>
) : GA4InfoBoxService {
override fun receiveDataPerWeekOfBox(): MailBox {
val today = DateTimeUtils.returnToday()
val sevenDaysAgo = today.minus(7, ChronoUnit.DAYS)
val startDate = getStartDate(sevenDaysAgo)
val endDate = getEndDate(today)
val exampleGA4Data = receiveData(startDate, endDate, exampleBlangcoGA4Service)
return exampleMailBoxBuilder.createMailBox(
ga4Data = exampleGA4Data,
period = ExtractPeriod.WEEK,
)
}
override fun receiveDataPerMonthOfBox(): MailBox {
val today = DateTimeUtils.returnToday()
val thirtyDaysAgo = today.minus(30, ChronoUnit.DAYS)
val startDate = getStartDate(thirtyDaysAgo)
val endDate = getEndDate(today)
val exampleGA4Data = receiveData(startDate, endDate, exampleBlangcoGA4Service)
return exampleMailBoxBuilder.createMailBox(
ga4Data = exampleGA4Data,
period = ExtractPeriod.MONTH,
)
}
νμΉν΄μ¨ json νμμΌλ‘ λ³ννλ κ³Όμ μ λ΄λΆμ μΌλ‘ createMailBox ν¨μλ‘μ μ²λ¦¬νμλ€.
μ€μΌμ₯΄λ¬λ₯Ό ν΅ν΄ μ§μ ν μ£Όμ μλ§λ€ λ°μ΄ν°λ₯Ό μ μ₯νλ€.
2. μ μ΄λ νλ² λ©μΌ μ μ‘
μ΄μ μ μ₯ν λ°μ΄ν°λ₯Ό κ°μ§κ³ , μ‘°ννμ¬ μ μ΄λ νλ² λ©μΌ μ μ‘μ μ§νν΄μΌ νλ€.
μ 체 μ½λλ λ무 λ§μΌλ―λ‘, μΌλΆ μΆμνλ ν΅μ¬ λ‘μ§λ§ μμ±νκ² λ€.
@Scheduled(cron = "0 0 9 ? * MON", zone = "Asia/Seoul")
fun sendGA4DataPerMonth() {
while (true) {
if (sendMailToGato(ExtractPeriod.MONTH).isEmpty()) {
break
}
}
}
private fun sendMailToClient(extractPeriod: ExtractPeriod): List<MailBox> {
val mailBoxes = getMailBoxes(extractPeriod)
if (mailBoxes.isNotEmpty()) {
val completedMailBoxIds = LinkedList<ObjectId>()
mailBoxes.forEach {
try {
exampleSendMailService.sendMail(
mailBox = it,
extractPeriod = extractPeriod
)
completedMailBoxIds.add(it.id)
} catch (e: Exception) {
logger.error(
"""Error occurred in ${Thread.currentThread().stackTrace[1].className}
at method ${Thread.currentThread().stackTrace[1].methodName}
line ${Thread.currentThread().stackTrace[1].lineNumber}: $ERROR_MESSAGE""",
e
)
mailBoxRepository.save(it)
}
}
if (completedMailBoxIds.isNotEmpty()) {
mailBoxRepository.deleteAllByIdIn(completedMailBoxIds)
}
}
return mailBoxes
}
μ μ‘νκ³ μ νλ λͺ¨λ λͺ©λ‘μ mongo μμ 쿼리ν΄μ€κ³ ,
λ°λ³΅λ¬Έμ ν΅ν΄ λ©μΌμ μΌλ¨ μ μ‘νλ€.
μ±κ³΅ν λͺ©λ‘λ€μ list collectionμ objectId λ§ λ΄λλ€.
μ€ν¨ν κ²½μ°, μ€ν¨ν μ΄λ²€νΈμ νν΄μ λ€μ μ μ₯νλ€.
λ§μ§λ§μΌλ‘, μ±κ³΅ μ΄λ²€νΈλ document μμ μμ νλ€.
μ΄ λ‘μ§μ μμ μ€μΌμ₯΄λ¬ ν¨μμμ while λ¬Έμ ν΅ν΄ boxes κ° empty ν λκΉμ§ μ§ννλ€.
κ²°λ‘
λ°μ΄ν°λ₯Ό κ°μ§κ³ μ μ΄λ νλ² λ©μΌ μ μ‘μ μ§νν μ μκΈ°μ, At least once λ₯Ό ꡬννλ€κ³ λ³Ό μ μλ€.
νμ§λ§, while λ¬Έμ ν΅ν΄ 무ν 루νλ₯Ό λκ² νλ λ°©μμ μλͺ» λ λ°©μμ΄λΌκ³ λ³Έλ€.
μλͺ»λ λ°μ΄ν°κ° κ³μμ μΌλ‘ λ€μ΄μμ μ μκΈ° λλ¬Έμ΄λ€.
μΌλΆ νμλ₯Ό μ ν λ€, κ·Έ μ΄μμ λ²μ΄λ κ²½μ°, μΌλ¨ μ€μΌμ₯΄λ¬λ μ’
λ£νκ³ ,
μ¬λ μλ¦Όμ΄λ, μλ¬ λ‘κ·Έλ₯Ό μμ±νλ λ°©μμ΄ μλ² λ¦¬μμ€ μ°¨μμμ μ’μ μμ λ°©μμΈ κ² κ°λ€.