From 640fd307c77ddbc23751e582c53f099a03208fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 17:37:11 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20kafkaProducerCon?= =?UTF-8?q?fig=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/KafkaProducerConfig.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt new file mode 100644 index 0000000..6391e62 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt @@ -0,0 +1,50 @@ +package hs.kr.entrydsm.user.infrastructure.kafka.configuration + +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.StringSerializer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.support.serializer.JsonSerializer + +@Configuration +class KafkaProducerConfig( + private val kafkaProperty: KafkaProperty +) { + + @Bean + fun deleteAllTableProducerFactory(): DefaultKafkaProducerFactory { + return DefaultKafkaProducerFactory(producerConfig()) + } + + @Bean + fun deleteAllTableKafkaTemplate(): KafkaTemplate { + return KafkaTemplate(deleteAllTableProducerFactory()) + } + + @Bean + fun deleteUserProducerFactory(): DefaultKafkaProducerFactory { + return DefaultKafkaProducerFactory(producerConfig()) + } + + @Bean + fun deleteUserKafkaTemplate(): KafkaTemplate { + return KafkaTemplate(deleteUserProducerFactory()) + } + + + private fun producerConfig(): Map { + return mapOf( + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to kafkaProperty.serverAddress, + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java, + "security.protocol" to "SASL_PLAINTEXT", + "sasl.mechanism" to "SCRAM-SHA-512", + "sasl.jaas.config" to + "org.apache.kafka.common.security.scram.ScramLoginModule required " + + "username=\"${kafkaProperty.confluentApiKey}\" " + + "password=\"${kafkaProperty.confluentApiSecret}\";" + ) + } +} \ No newline at end of file From 34297ff8b92571bce1dfb47e6f89fb96b0c6fe1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 17:37:45 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20kafka=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- buildSrc/src/main/kotlin/Dependencies.kt | 4 +++- buildSrc/src/main/kotlin/Plugin.kt | 1 + casper-user/build.gradle.kts | 6 +++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 93c250f..c3e4558 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -50,5 +50,7 @@ object Dependencies { const val SENTRY_SPRING_BOOT_STARTER = "io.sentry:sentry-spring-boot-starter-jakarta:${DependencyVersion.SENTRY}" // Spring Cloud Config - const val SPRING_CLOUD_STARTER_CONFIG = "org.springframework.cloud:spring-cloud-starter-config" + const val SPRING_CLOUD_STARTER_CONFIG = "org.springframework.cloud:spring-cloud-starter:2024.0.2" + + const val KAFKA = "org.springframework.kafka:spring-kafka" } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Plugin.kt b/buildSrc/src/main/kotlin/Plugin.kt index eb910e9..ed4563e 100644 --- a/buildSrc/src/main/kotlin/Plugin.kt +++ b/buildSrc/src/main/kotlin/Plugin.kt @@ -1,6 +1,7 @@ object Plugin { const val KOTLIN_JVM = "org.jetbrains.kotlin.jvm" const val KOTLIN_SPRING = "org.jetbrains.kotlin.plugin.spring" + const val KOTLIN_JPA = "org.jetbrains.kotlin.plugin.jpa" const val KOTLIN_KAPT = "org.jetbrains.kotlin.kapt" const val SPRING_BOOT = "org.springframework.boot" const val SPRING_DEPENDENCY_MANAGEMENT = "io.spring.dependency-management" diff --git a/casper-user/build.gradle.kts b/casper-user/build.gradle.kts index 31bb7ed..cf8e149 100644 --- a/casper-user/build.gradle.kts +++ b/casper-user/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id(Plugin.KOTLIN_JVM) version PluginVersion.KOTLIN_VERSION id(Plugin.KOTLIN_SPRING) version PluginVersion.KOTLIN_VERSION + id(Plugin.KOTLIN_JPA) version PluginVersion.KOTLIN_VERSION id(Plugin.KOTLIN_KAPT) id(Plugin.SPRING_BOOT) version PluginVersion.SPRING_BOOT_VERSION id(Plugin.SPRING_DEPENDENCY_MANAGEMENT) version PluginVersion.SPRING_DEPENDENCY_MANAGEMENT_VERSION @@ -78,8 +79,11 @@ dependencies { // Sentry implementation(Dependencies.SENTRY_SPRING_BOOT_STARTER) + //kafka + implementation(Dependencies.KAFKA) + // Spring Cloud Config - implementation(Dependencies.SPRING_CLOUD_STARTER_CONFIG) + //implementation(Dependencies.SPRING_CLOUD_STARTER_CONFIG) } protobuf { From bc61ac30324eb09754021ea34182510486f9c7c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 17:38:26 +0900 Subject: [PATCH 03/20] =?UTF-8?q?chore=20(=20#33=20)=20:=20=EC=9B=90?= =?UTF-8?q?=EB=9E=98=20=EC=9E=88=EB=8D=98=20Mock=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85=EB=90=9C=20Producer=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kafka/producer/MockDeleteAllTableProducer.kt | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/MockDeleteAllTableProducer.kt diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/MockDeleteAllTableProducer.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/MockDeleteAllTableProducer.kt deleted file mode 100644 index 0ebc1b9..0000000 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/MockDeleteAllTableProducer.kt +++ /dev/null @@ -1,16 +0,0 @@ -package hs.kr.entrydsm.user.infrastructure.kafka.producer - -import org.springframework.context.annotation.Profile -import org.springframework.stereotype.Component - -/** - * Kafka가 도입되기 전까지 사용할 임시 Mock Producer입니다. - */ -@Component -@Profile("!kafka") -class MockDeleteAllTableProducer : DeleteAllTableProducer { - override fun send() { - // TODO: Kafka 도입 후 실제 구현체로 교체 - println("Mock: DeleteAllTable event sent (Kafka not available)") - } -} \ No newline at end of file From b0e4cab7a7447134d16d06d411b9caffc242552b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 17:39:03 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20deleteAll=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/out/persistence/UserPersistenceAdapter.kt | 8 ++++++++ .../domain/user/application/port/out/DeleteUserPort.kt | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/adapter/out/persistence/UserPersistenceAdapter.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/adapter/out/persistence/UserPersistenceAdapter.kt index c3f4f06..7fb4cf7 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/adapter/out/persistence/UserPersistenceAdapter.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/adapter/out/persistence/UserPersistenceAdapter.kt @@ -81,6 +81,14 @@ class UserPersistenceAdapter( userRepository.deleteById(userId) } + /** + * 모든 사용자를 삭제합니다. + * 관리자의 전체 데이터 초기화 시에만 사용됩니다. + */ + override fun deleteAll() { + userRepository.deleteAll() + } + /** * 지정된 일수보다 오래된 탈퇴 사용자를 조회합니다. * diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/application/port/out/DeleteUserPort.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/application/port/out/DeleteUserPort.kt index 15cf42c..882fa19 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/application/port/out/DeleteUserPort.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/application/port/out/DeleteUserPort.kt @@ -22,4 +22,10 @@ interface DeleteUserPort { * @return 삭제 대상 사용자 목록 */ fun findWithdrawnUsersOlderThan(days: Long): List + + /** + * 모든 사용자를 삭제합니다. + * 관리자의 전체 데이터 초기화 시에만 사용됩니다. + */ + fun deleteAll() } From b2a419c667c9cfaf3b91eebccdf25cb7c6062b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 17:39:16 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20consumer=20confi?= =?UTF-8?q?g=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/KafkaConsumerConfig.kt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaConsumerConfig.kt diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaConsumerConfig.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaConsumerConfig.kt new file mode 100644 index 0000000..69d9a5f --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaConsumerConfig.kt @@ -0,0 +1,45 @@ +package hs.kr.entrydsm.user.infrastructure.kafka.configuration + +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.common.serialization.StringDeserializer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.annotation.EnableKafka +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.support.serializer.JsonDeserializer + +@EnableKafka +@Configuration +class KafkaConsumerConfig( + private val kafkaProperty: KafkaProperty +) { + + @Bean + fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory { + return ConcurrentKafkaListenerContainerFactory().apply { + setConcurrency(2) + consumerFactory = DefaultKafkaConsumerFactory(consumerFactoryConfig()) + containerProperties.pollTimeout = 500 + } + } + + private fun consumerFactoryConfig(): Map { + return mapOf( + ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to kafkaProperty.serverAddress, + ConsumerConfig.ISOLATION_LEVEL_CONFIG to "read_committed", + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG to "false", + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "latest", + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java, + ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG to 5000, + JsonDeserializer.TRUSTED_PACKAGES to "*", + "security.protocol" to "SASL_PLAINTEXT", + "sasl.mechanism" to "SCRAM-SHA-512", + "sasl.jaas.config" to + "org.apache.kafka.common.security.scram.ScramLoginModule required " + + "username=\"${kafkaProperty.confluentApiKey}\" " + + "password=\"${kafkaProperty.confluentApiSecret}\";" + ) + } +} \ No newline at end of file From 3aca7b33adc25ef63e9c8d843001321ac9af4e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 17:39:28 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20kafkaProperty=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kafka/configuration/KafkaProperty.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProperty.kt diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProperty.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProperty.kt new file mode 100644 index 0000000..44f0f4e --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProperty.kt @@ -0,0 +1,12 @@ +package hs.kr.entrydsm.user.infrastructure.kafka.configuration + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.ConfigurationPropertiesBinding + +@ConfigurationPropertiesBinding +@ConfigurationProperties("kafka") +class KafkaProperty( + val serverAddress: String, + val confluentApiKey: String, + val confluentApiSecret: String +) \ No newline at end of file From 1dcada7986fe990a5ceeda810f339fe85b67be47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 17:40:15 +0900 Subject: [PATCH 07/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20kafkaTopic=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/infrastructure/kafka/configuration/KafkaTopics.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaTopics.kt diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaTopics.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaTopics.kt new file mode 100644 index 0000000..0d60e09 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaTopics.kt @@ -0,0 +1,7 @@ +package hs.kr.entrydsm.user.infrastructure.kafka.configuration + +object KafkaTopics { + const val DELETE_USER = "delete-user" + const val DELETE_ALL_TABLE = "delete-all-table" + const val CREATE_APPLICATION = "create-application" +} \ No newline at end of file From 0cd432096b4a356f97276ff3a2a2c13c521e6cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 17:40:25 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20consumer=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../consumer/CreateApplicationConsumer.kt | 25 +++++++++++++++++++ .../kafka/consumer/DeleteUserTableConsumer.kt | 19 ++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/CreateApplicationConsumer.kt create mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/DeleteUserTableConsumer.kt diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/CreateApplicationConsumer.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/CreateApplicationConsumer.kt new file mode 100644 index 0000000..f88d0d3 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/CreateApplicationConsumer.kt @@ -0,0 +1,25 @@ +package hs.kr.entrydsm.user.infrastructure.kafka.consumer + +import com.fasterxml.jackson.databind.ObjectMapper +import hs.kr.entrydsm.user.domain.user.application.port.`in`.ChangeReceiptCodeUseCase +import hs.kr.entrydsm.user.infrastructure.kafka.configuration.KafkaTopics +import hs.kr.entrydsm.user.infrastructure.kafka.consumer.dto.CreateApplicationEvent +import org.springframework.kafka.annotation.KafkaListener +import org.springframework.stereotype.Service + +@Service +class CreateApplicationConsumer( + private val changeReceiptCodeUseCase: ChangeReceiptCodeUseCase, + private val mapper: ObjectMapper +) { + @KafkaListener( + topics = [KafkaTopics.CREATE_APPLICATION], + groupId = "change-user-receipt-code-consumer", + containerFactory = "kafkaListenerContainerFactory" + ) + fun execute(message: String) { + val createApplicationEvent = mapper.readValue(message, CreateApplicationEvent::class.java) + changeReceiptCodeUseCase.changeReceiptCode(createApplicationEvent.userId, createApplicationEvent.receiptCode) + } + +} \ No newline at end of file diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/DeleteUserTableConsumer.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/DeleteUserTableConsumer.kt new file mode 100644 index 0000000..6475f48 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/DeleteUserTableConsumer.kt @@ -0,0 +1,19 @@ +package hs.kr.entrydsm.user.infrastructure.kafka.consumer + +import hs.kr.entrydsm.user.domain.user.application.port.out.DeleteUserPort +import hs.kr.entrydsm.user.infrastructure.kafka.configuration.KafkaTopics +import org.springframework.kafka.annotation.KafkaListener +import org.springframework.transaction.annotation.Transactional + +open class DeleteUserTableConsumer( + private val deleteUserPort: DeleteUserPort +) { + @KafkaListener( + topics = [KafkaTopics.DELETE_ALL_TABLE], + groupId = "delete-all-table-user", + containerFactory = "kafkaListenerContainerFactory" + ) + @Transactional + open fun execute() = deleteUserPort.deleteAll() + +} \ No newline at end of file From 996e4b661645498ddd641d9c27e05d6e33c721fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 17:40:48 +0900 Subject: [PATCH 09/20] =?UTF-8?q?refactor=20(=20#33=20)=20:=20=EC=9B=90?= =?UTF-8?q?=EB=9E=98=20Mock=EC=9C=BC=EB=A1=9C=20=EC=A3=BC=EC=9E=85?= =?UTF-8?q?=EB=90=9C=20Producer=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kafka/producer/MockDeleteUserProducer.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/MockDeleteUserProducer.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/MockDeleteUserProducer.kt index 23072b5..f49b5c0 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/MockDeleteUserProducer.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/MockDeleteUserProducer.kt @@ -1,16 +1,16 @@ package hs.kr.entrydsm.user.infrastructure.kafka.producer +import hs.kr.entrydsm.user.infrastructure.kafka.configuration.KafkaTopics import org.springframework.context.annotation.Profile +import org.springframework.kafka.core.KafkaTemplate import org.springframework.stereotype.Component -/** - * Kafka가 도입되기 전까지 사용할 임시 Mock Producer입니다. - */ + @Component -@Profile("!kafka") -class MockDeleteUserProducer : DeleteUserProducer { +class MockDeleteUserProducer( + private val kafkaTemplate: KafkaTemplate +) : DeleteUserProducer { override fun send(receiptCode: Long) { - // TODO: Kafka 도입 후 실제 구현체로 교체 - println("Mock: DeleteUser event sent for receiptCode: $receiptCode (Kafka not available)") + kafkaTemplate.send(KafkaTopics.DELETE_USER, receiptCode) } } \ No newline at end of file From f146848407299916be9dd1963b702ba9ecfd6c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 17:41:25 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20DeleteAllTablePr?= =?UTF-8?q?oducer=20=EA=B5=AC=ED=98=84=EC=B2=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../producer/DeleteAllTableProducerImpl.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteAllTableProducerImpl.kt diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteAllTableProducerImpl.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteAllTableProducerImpl.kt new file mode 100644 index 0000000..e3551bf --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteAllTableProducerImpl.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.user.infrastructure.kafka.producer + +import hs.kr.entrydsm.user.infrastructure.kafka.configuration.KafkaTopics +import org.springframework.context.annotation.Profile +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.stereotype.Component + + +@Component +class DeleteAllTableProducerImpl( + private val kafkaTemplate: KafkaTemplate, +) : DeleteAllTableProducer { + + override fun send() { + kafkaTemplate.send(KafkaTopics.DELETE_ALL_TABLE, Unit) + } +} From b2797de3f28acb32309475c92acbb61d8a9df469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 17:41:43 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20CreateApplicatio?= =?UTF-8?q?nEvent=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kafka/consumer/dto/CreateApplicationEvent.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/CreateApplicationEvent.kt diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/CreateApplicationEvent.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/CreateApplicationEvent.kt new file mode 100644 index 0000000..f1dd058 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/CreateApplicationEvent.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.user.infrastructure.kafka.consumer.dto + +import java.util.UUID + +data class CreateApplicationEvent( + val receiptCode: Long, + val userId: UUID +) \ No newline at end of file From 264a0483a97323d5761c28ebd66de1ffd0216e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 18:31:08 +0900 Subject: [PATCH 12/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20=EC=82=AC?= =?UTF-8?q?=EA=B0=80=20=ED=8C=A8=ED=84=B4=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../consumer/dto/UserReceiptCodeUpdateCompletedEvent.kt | 8 ++++++++ .../consumer/dto/UserReceiptCodeUpdateFailedEvent.kt | 9 +++++++++ 2 files changed, 17 insertions(+) create mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateCompletedEvent.kt create mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateFailedEvent.kt diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateCompletedEvent.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateCompletedEvent.kt new file mode 100644 index 0000000..ca8fec8 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateCompletedEvent.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.user.infrastructure.kafka.consumer.dto + +import java.util.UUID + +data class UserReceiptCodeUpdateCompletedEvent( + val receiptCode: Long, + val userId: UUID +) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateFailedEvent.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateFailedEvent.kt new file mode 100644 index 0000000..fac1297 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateFailedEvent.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.user.infrastructure.kafka.consumer.dto + +import java.util.UUID + +data class UserReceiptCodeUpdateFailedEvent( + val receiptCode: Long, + val userId: UUID, + val reason: String +) From a89992b49413011f38bdefaffc2e91d3b5677713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 19:50:43 +0900 Subject: [PATCH 13/20] =?UTF-8?q?refactor=20(=20#33=20)=20:=20saga=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=EC=97=90=20=EB=B6=80=ED=95=A9=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ChangeReceiptCodeService.kt | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/application/service/ChangeReceiptCodeService.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/application/service/ChangeReceiptCodeService.kt index 26e17b2..cc3720c 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/application/service/ChangeReceiptCodeService.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/application/service/ChangeReceiptCodeService.kt @@ -4,6 +4,7 @@ import hs.kr.entrydsm.user.domain.user.application.port.`in`.ChangeReceiptCodeUs import hs.kr.entrydsm.user.domain.user.application.port.out.QueryUserPort import hs.kr.entrydsm.user.domain.user.application.port.out.SaveUserPort import hs.kr.entrydsm.user.domain.user.exception.UserNotFoundException +import hs.kr.entrydsm.user.infrastructure.kafka.producer.UserEventProducer import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.util.UUID @@ -14,12 +15,13 @@ import java.util.UUID * * @property queryUserPort 사용자 조회 포트 * @property saveUserPort 사용자 저장 포트 + * @property userEventProducer 사용자 이벤트 발행기 */ -@Transactional @Service class ChangeReceiptCodeService( private val queryUserPort: QueryUserPort, private val saveUserPort: SaveUserPort, + private val userEventProducer: UserEventProducer ) : ChangeReceiptCodeUseCase { /** * 사용자의 접수코드를 변경합니다. @@ -28,12 +30,39 @@ class ChangeReceiptCodeService( * @param receiptCode 새로운 접수코드 * @throws UserNotFoundException 사용자가 존재하지 않는 경우 */ + @Transactional override fun changeReceiptCode( userId: UUID, receiptCode: Long, ) { - val user = queryUserPort.findById(userId) ?: throw UserNotFoundException - val updateUser = user.changeReceiptCode(receiptCode) - saveUserPort.save(updateUser) + try { + val user = queryUserPort.findById(userId) + + if (user == null) { + userEventProducer.sendReceiptCodeUpdateFailed( + receiptCode = receiptCode, + userId = userId, + reason = "User not found" + ) + throw UserNotFoundException + } + + val updatedUser = user.copy(receiptCode = receiptCode) + saveUserPort.save(updatedUser) + + // 성공 이벤트 발행 + userEventProducer.sendReceiptCodeUpdateCompleted(receiptCode, userId) + + } catch (e: Exception) { + + if (e !is UserNotFoundException) { + userEventProducer.sendReceiptCodeUpdateFailed( + receiptCode = receiptCode, + userId = userId, + reason = e.message ?: "Unknown error" + ) + } + throw e // 예외 다시 던져서 롤백 발생 + } } } From 9ab18742a0c26769a8729fcbfae0b654349b73c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 19:50:58 +0900 Subject: [PATCH 14/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20config=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kafka/configuration/KafkaProducerConfig.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt index 6391e62..b8f0e43 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt @@ -33,6 +33,16 @@ class KafkaProducerConfig( return KafkaTemplate(deleteUserProducerFactory()) } + @Bean + fun userEventProducerFactory(): DefaultKafkaProducerFactory { + return DefaultKafkaProducerFactory(producerConfig()) + } + + @Bean + fun userEventKafkaTemplate(): KafkaTemplate { + return KafkaTemplate(userEventProducerFactory()) + } + private fun producerConfig(): Map { return mapOf( From d43a292a5b66ffe161be61b5502e26fe34deb04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 19:51:10 +0900 Subject: [PATCH 15/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20topics=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/infrastructure/kafka/configuration/KafkaTopics.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaTopics.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaTopics.kt index 0d60e09..663dd2b 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaTopics.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaTopics.kt @@ -4,4 +4,8 @@ object KafkaTopics { const val DELETE_USER = "delete-user" const val DELETE_ALL_TABLE = "delete-all-table" const val CREATE_APPLICATION = "create-application" + + // Choreography 이벤트들 + const val USER_RECEIPT_CODE_UPDATE_COMPLETED = "user-receipt-code-update-completed" + const val USER_RECEIPT_CODE_UPDATE_FAILED = "user-receipt-code-update-failed" } \ No newline at end of file From 999fcadbf045650dfc05b2f982ebdd9e77fa5a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 19:51:29 +0900 Subject: [PATCH 16/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20userEventProduce?= =?UTF-8?q?r=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/kafka/producer/UserEventProducer.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducer.kt diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducer.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducer.kt new file mode 100644 index 0000000..91543ed --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducer.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.user.infrastructure.kafka.producer + +import java.util.UUID + +interface UserEventProducer { + fun sendReceiptCodeUpdateCompleted(receiptCode: Long, userId: UUID) + fun sendReceiptCodeUpdateFailed(receiptCode: Long, userId: UUID, reason: String) +} \ No newline at end of file From 21c5394cbb2b60324053953fbb27fbceb55cd21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 19:51:42 +0900 Subject: [PATCH 17/20] =?UTF-8?q?feat=20=20(=20#33=20)=20:=20producerImpl?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kafka/producer/UserEventProducerImpl.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducerImpl.kt diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducerImpl.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducerImpl.kt new file mode 100644 index 0000000..f68f3b2 --- /dev/null +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducerImpl.kt @@ -0,0 +1,25 @@ +package hs.kr.entrydsm.user.infrastructure.kafka.producer + +import hs.kr.entrydsm.user.infrastructure.kafka.configuration.KafkaTopics +import hs.kr.entrydsm.user.infrastructure.kafka.consumer.dto.UserReceiptCodeUpdateCompletedEvent +import hs.kr.entrydsm.user.infrastructure.kafka.consumer.dto.UserReceiptCodeUpdateFailedEvent +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class UserEventProducerImpl( + private val userEventKafkaTemplate: KafkaTemplate, +): UserEventProducer { + + override fun sendReceiptCodeUpdateCompleted(receiptCode: Long, userId: UUID) { + val event = UserReceiptCodeUpdateCompletedEvent(receiptCode, userId) + userEventKafkaTemplate.send(KafkaTopics.USER_RECEIPT_CODE_UPDATE_COMPLETED, event) + } + + override fun sendReceiptCodeUpdateFailed(receiptCode: Long, userId: UUID, reason: String) { + val event = UserReceiptCodeUpdateFailedEvent(receiptCode, userId, reason) + userEventKafkaTemplate.send(KafkaTopics.USER_RECEIPT_CODE_UPDATE_FAILED, event) + } + +} \ No newline at end of file From 3e7c3e38d9ab08a0eedc319fc593d8b9117f158d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 19:51:56 +0900 Subject: [PATCH 18/20] =?UTF-8?q?chore=20(=20#33=20)=20:=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EB=84=A4=EC=9E=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{MockDeleteUserProducer.kt => DeleteUserProducerImpl.kt} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/{MockDeleteUserProducer.kt => DeleteUserProducerImpl.kt} (84%) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/MockDeleteUserProducer.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteUserProducerImpl.kt similarity index 84% rename from casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/MockDeleteUserProducer.kt rename to casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteUserProducerImpl.kt index f49b5c0..7610946 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/MockDeleteUserProducer.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteUserProducerImpl.kt @@ -1,13 +1,12 @@ package hs.kr.entrydsm.user.infrastructure.kafka.producer import hs.kr.entrydsm.user.infrastructure.kafka.configuration.KafkaTopics -import org.springframework.context.annotation.Profile import org.springframework.kafka.core.KafkaTemplate import org.springframework.stereotype.Component @Component -class MockDeleteUserProducer( +class DeleteUserProducerImpl( private val kafkaTemplate: KafkaTemplate ) : DeleteUserProducer { override fun send(receiptCode: Long) { From ca54f7200a7abb33ea81961b9897539dcbcc98c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Wed, 3 Sep 2025 20:48:26 +0900 Subject: [PATCH 19/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=BB=A4=EB=B0=8B=20=ED=9B=84=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/ChangeReceiptCodeService.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/application/service/ChangeReceiptCodeService.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/application/service/ChangeReceiptCodeService.kt index cc3720c..0f102a5 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/application/service/ChangeReceiptCodeService.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/domain/user/application/service/ChangeReceiptCodeService.kt @@ -5,8 +5,11 @@ import hs.kr.entrydsm.user.domain.user.application.port.out.QueryUserPort import hs.kr.entrydsm.user.domain.user.application.port.out.SaveUserPort import hs.kr.entrydsm.user.domain.user.exception.UserNotFoundException import hs.kr.entrydsm.user.infrastructure.kafka.producer.UserEventProducer +import jakarta.transaction.Synchronization import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.support.TransactionSynchronization +import org.springframework.transaction.support.TransactionSynchronizationManager import java.util.UUID /** @@ -50,8 +53,12 @@ class ChangeReceiptCodeService( val updatedUser = user.copy(receiptCode = receiptCode) saveUserPort.save(updatedUser) - // 성공 이벤트 발행 - userEventProducer.sendReceiptCodeUpdateCompleted(receiptCode, userId) + TransactionSynchronizationManager.registerSynchronization(object : TransactionSynchronization { + override fun afterCommit() { + userEventProducer.sendReceiptCodeUpdateCompleted(receiptCode, userId) + } + }) + } catch (e: Exception) { From 1b14e14cff354aa74090017f83e2cb7d0565d17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=9B=90?= Date: Thu, 4 Sep 2025 00:54:56 +0900 Subject: [PATCH 20/20] =?UTF-8?q?feat=20(=20#33=20)=20:=20kdoc=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/KafkaConsumerConfig.kt | 26 +++++++++- .../configuration/KafkaProducerConfig.kt | 49 ++++++++++++++++++- .../kafka/configuration/KafkaProperty.kt | 12 ++++- .../kafka/configuration/KafkaTopics.kt | 33 ++++++++++++- .../kafka/consumer/DeleteUserTableConsumer.kt | 17 ++++++- .../consumer/dto/CreateApplicationEvent.kt | 11 ++++- .../UserReceiptCodeUpdateCompletedEvent.kt | 9 ++++ .../dto/UserReceiptCodeUpdateFailedEvent.kt | 11 +++++ .../producer/DeleteAllTableProducerImpl.kt | 15 +++++- .../kafka/producer/DeleteUserProducerImpl.kt | 20 +++++++- .../kafka/producer/UserEventProducer.kt | 23 ++++++++- .../kafka/producer/UserEventProducerImpl.kt | 29 ++++++++++- 12 files changed, 243 insertions(+), 12 deletions(-) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaConsumerConfig.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaConsumerConfig.kt index 69d9a5f..db0dc8f 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaConsumerConfig.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaConsumerConfig.kt @@ -9,12 +9,28 @@ import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory import org.springframework.kafka.core.DefaultKafkaConsumerFactory import org.springframework.kafka.support.serializer.JsonDeserializer +/** + * Kafka Consumer 설정을 담당하는 Configuration 클래스입니다. + * + * 원서 생성 이벤트 수신을 위한 Consumer 설정을 구성하며, + * Confluent Cloud 연결을 위한 보안 설정을 포함합니다. + * + * @property kafkaProperty Kafka 연결 정보를 담은 프로퍼티 + */ @EnableKafka @Configuration class KafkaConsumerConfig( private val kafkaProperty: KafkaProperty ) { + /** + * Kafka 리스너 컨테이너 팩토리를 생성합니다. + * + * 동시성 레벨을 2로 설정하여 병렬 메시지 처리를 지원하며, + * 폴링 타임아웃을 500ms로 설정하여 적절한 응답성을 보장합니다. + * + * @return 설정된 ConcurrentKafkaListenerContainerFactory 인스턴스 + */ @Bean fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory { return ConcurrentKafkaListenerContainerFactory().apply { @@ -24,6 +40,14 @@ class KafkaConsumerConfig( } } + /** + * Kafka Consumer의 기본 설정을 구성합니다. + * + * Confluent Cloud 연결을 위한 SASL 보안 설정과 역직렬화 설정을 포함하며, + * read_committed 격리 레벨로 트랜잭션 안정성을 보장합니다. + * + * @return Consumer 설정 맵 + */ private fun consumerFactoryConfig(): Map { return mapOf( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to kafkaProperty.serverAddress, @@ -42,4 +66,4 @@ class KafkaConsumerConfig( "password=\"${kafkaProperty.confluentApiSecret}\";" ) } -} \ No newline at end of file +} diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt index b8f0e43..1759cb3 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt @@ -8,42 +8,87 @@ import org.springframework.kafka.core.DefaultKafkaProducerFactory import org.springframework.kafka.core.KafkaTemplate import org.springframework.kafka.support.serializer.JsonSerializer +/** + * Kafka Producer 설정을 담당하는 Configuration 클래스입니다. + * + * 사용자 삭제, 전체 테이블 삭제, 사용자 이벤트 발행을 위한 + * 각각의 KafkaTemplate과 ProducerFactory를 구성하며, + * Confluent Cloud 연결을 위한 보안 설정을 포함합니다. + * + * @property kafkaProperty Kafka 연결 정보를 담은 프로퍼티 + */ @Configuration class KafkaProducerConfig( private val kafkaProperty: KafkaProperty ) { + /** + * 전체 테이블 삭제 이벤트용 Producer Factory를 생성합니다. + * + * @return Unit 타입 메시지용 DefaultKafkaProducerFactory + */ @Bean fun deleteAllTableProducerFactory(): DefaultKafkaProducerFactory { return DefaultKafkaProducerFactory(producerConfig()) } + /** + * 전체 테이블 삭제 이벤트 발행을 위한 KafkaTemplate을 생성합니다. + * + * @return Unit 타입 메시지용 KafkaTemplate + */ @Bean fun deleteAllTableKafkaTemplate(): KafkaTemplate { return KafkaTemplate(deleteAllTableProducerFactory()) } + /** + * 사용자 삭제 이벤트용 Producer Factory를 생성합니다. + * + * @return Long 타입 메시지용 DefaultKafkaProducerFactory + */ @Bean fun deleteUserProducerFactory(): DefaultKafkaProducerFactory { return DefaultKafkaProducerFactory(producerConfig()) } + /** + * 사용자 삭제 이벤트 발행을 위한 KafkaTemplate을 생성합니다. + * + * @return Long 타입 메시지용 KafkaTemplate + */ @Bean fun deleteUserKafkaTemplate(): KafkaTemplate { return KafkaTemplate(deleteUserProducerFactory()) } + /** + * 사용자 이벤트용 Producer Factory를 생성합니다. + * + * @return Any 타입 메시지용 DefaultKafkaProducerFactory + */ @Bean fun userEventProducerFactory(): DefaultKafkaProducerFactory { return DefaultKafkaProducerFactory(producerConfig()) } + /** + * 사용자 이벤트 발행을 위한 KafkaTemplate을 생성합니다. + * + * @return Any 타입 메시지용 KafkaTemplate + */ @Bean fun userEventKafkaTemplate(): KafkaTemplate { return KafkaTemplate(userEventProducerFactory()) } - + /** + * Kafka Producer의 기본 설정을 구성합니다. + * + * Confluent Cloud 연결을 위한 SASL 보안 설정과 직렬화 설정을 포함합니다. + * + * @return Producer 설정 맵 + */ private fun producerConfig(): Map { return mapOf( ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to kafkaProperty.serverAddress, @@ -57,4 +102,4 @@ class KafkaProducerConfig( "password=\"${kafkaProperty.confluentApiSecret}\";" ) } -} \ No newline at end of file +} diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProperty.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProperty.kt index 44f0f4e..4f33502 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProperty.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProperty.kt @@ -3,10 +3,20 @@ package hs.kr.entrydsm.user.infrastructure.kafka.configuration import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationPropertiesBinding +/** + * Kafka 연결을 위한 설정 프로퍼티 클래스입니다. + * + * application.yml의 kafka 섹션에서 설정값을 바인딩하여 + * Confluent Cloud Kafka 클러스터 연결에 필요한 정보를 관리합니다. + * + * @property serverAddress Kafka 브로커 서버 주소 + * @property confluentApiKey Confluent Cloud 접근을 위한 API 키 + * @property confluentApiSecret Confluent Cloud 접근을 위한 API 시크릿 + */ @ConfigurationPropertiesBinding @ConfigurationProperties("kafka") class KafkaProperty( val serverAddress: String, val confluentApiKey: String, val confluentApiSecret: String -) \ No newline at end of file +) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaTopics.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaTopics.kt index 663dd2b..1e138a7 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaTopics.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaTopics.kt @@ -1,11 +1,42 @@ package hs.kr.entrydsm.user.infrastructure.kafka.configuration +/** + * Kafka 토픽명을 관리하는 상수 객체입니다. + * + * 마이크로서비스 간 이벤트 통신에 사용되는 토픽명들을 중앙에서 관리하여 + * 토픽명 변경 시 일관성을 보장합니다. + */ object KafkaTopics { + /** + * 사용자 삭제 이벤트 토픽 + * 사용자가 탈퇴했을 때 다른 서비스에 알리기 위해 사용 + */ const val DELETE_USER = "delete-user" + + /** + * 전체 테이블 삭제 이벤트 토픽 + * 관리자가 전체 데이터를 초기화할 때 다른 서비스에 알리기 위해 사용 + */ const val DELETE_ALL_TABLE = "delete-all-table" + + /** + * 원서 생성 이벤트 토픽 + * 원서 서비스에서 원서가 생성되었을 때 사용자 접수번호 업데이트를 위해 사용 + */ const val CREATE_APPLICATION = "create-application" // Choreography 이벤트들 + + /** + * 사용자 접수번호 업데이트 완료 이벤트 토픽 + * 사용자 서비스에서 접수번호 업데이트가 성공적으로 완료되었음을 알리는 이벤트 + */ const val USER_RECEIPT_CODE_UPDATE_COMPLETED = "user-receipt-code-update-completed" + + /** + * 사용자 접수번호 업데이트 실패 이벤트 토픽 + * 사용자 서비스에서 접수번호 업데이트가 실패했음을 알리는 이벤트 + * 원서 서비스에서 이 이벤트를 수신하면 보상 트랜잭션을 수행함 + */ const val USER_RECEIPT_CODE_UPDATE_FAILED = "user-receipt-code-update-failed" -} \ No newline at end of file +} diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/DeleteUserTableConsumer.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/DeleteUserTableConsumer.kt index 6475f48..a0ca83b 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/DeleteUserTableConsumer.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/DeleteUserTableConsumer.kt @@ -5,9 +5,24 @@ import hs.kr.entrydsm.user.infrastructure.kafka.configuration.KafkaTopics import org.springframework.kafka.annotation.KafkaListener import org.springframework.transaction.annotation.Transactional +/** + * 전체 테이블 삭제 이벤트를 수신하여 사용자 테이블을 삭제하는 Consumer 클래스입니다. + * + * 관리자가 전체 데이터 초기화를 요청했을 때 사용자 테이블의 모든 데이터를 + * 삭제하는 역할을 담당합니다. + * + * @property deleteUserPort 사용자 삭제 작업을 수행하는 포트 + */ open class DeleteUserTableConsumer( private val deleteUserPort: DeleteUserPort ) { + + /** + * 전체 테이블 삭제 이벤트를 수신하여 모든 사용자 데이터를 삭제합니다. + * + * DELETE_ALL_TABLE 토픽에서 이벤트를 수신하고, + * 트랜잭션 내에서 모든 사용자 데이터를 삭제합니다. + */ @KafkaListener( topics = [KafkaTopics.DELETE_ALL_TABLE], groupId = "delete-all-table-user", @@ -16,4 +31,4 @@ open class DeleteUserTableConsumer( @Transactional open fun execute() = deleteUserPort.deleteAll() -} \ No newline at end of file +} diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/CreateApplicationEvent.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/CreateApplicationEvent.kt index f1dd058..2e756f5 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/CreateApplicationEvent.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/CreateApplicationEvent.kt @@ -2,7 +2,16 @@ package hs.kr.entrydsm.user.infrastructure.kafka.consumer.dto import java.util.UUID +/** + * 원서 생성 이벤트 데이터를 담는 DTO 클래스입니다. + * + * 원서 서비스에서 원서가 생성되었을 때 사용자 서비스에 전달되는 + * 이벤트 데이터를 정의합니다. 사용자의 접수번호 업데이트를 위해 사용됩니다. + * + * @property receiptCode 생성된 원서의 접수번호 + * @property userId 원서를 생성한 사용자의 고유 식별자 + */ data class CreateApplicationEvent( val receiptCode: Long, val userId: UUID -) \ No newline at end of file +) diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateCompletedEvent.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateCompletedEvent.kt index ca8fec8..f4d37e0 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateCompletedEvent.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateCompletedEvent.kt @@ -2,6 +2,15 @@ package hs.kr.entrydsm.user.infrastructure.kafka.consumer.dto import java.util.UUID +/** + * 사용자 접수번호 업데이트 완료 이벤트 데이터를 담는 DTO 클래스입니다. + * + * 사용자 서비스에서 접수번호 업데이트가 성공적으로 완료되었을 때 + * 원서 서비스에 전달하는 이벤트 데이터를 정의합니다. + * + * @property receiptCode 업데이트된 접수번호 + * @property userId 접수번호가 업데이트된 사용자의 고유 식별자 + */ data class UserReceiptCodeUpdateCompletedEvent( val receiptCode: Long, val userId: UUID diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateFailedEvent.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateFailedEvent.kt index fac1297..052a84f 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateFailedEvent.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/consumer/dto/UserReceiptCodeUpdateFailedEvent.kt @@ -2,6 +2,17 @@ package hs.kr.entrydsm.user.infrastructure.kafka.consumer.dto import java.util.UUID +/** + * 사용자 접수번호 업데이트 실패 이벤트 데이터를 담는 DTO 클래스입니다. + * + * 사용자 서비스에서 접수번호 업데이트가 실패했을 때 원서 서비스에 전달하는 + * 이벤트 데이터를 정의합니다. 원서 서비스에서는 이 이벤트를 수신하여 + * 보상 트랜잭션으로 해당 원서를 삭제합니다. + * + * @property receiptCode 업데이트 실패한 접수번호 + * @property userId 접수번호 업데이트가 실패한 사용자의 고유 식별자 + * @property reason 업데이트 실패 사유 + */ data class UserReceiptCodeUpdateFailedEvent( val receiptCode: Long, val userId: UUID, diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteAllTableProducerImpl.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteAllTableProducerImpl.kt index e3551bf..1c9f002 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteAllTableProducerImpl.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteAllTableProducerImpl.kt @@ -5,12 +5,25 @@ import org.springframework.context.annotation.Profile import org.springframework.kafka.core.KafkaTemplate import org.springframework.stereotype.Component - +/** + * 전체 테이블 삭제 이벤트를 발행하는 Producer 구현체입니다. + * + * 관리자가 전체 데이터를 초기화할 때 다른 마이크로서비스에 알려 + * 모든 관련 데이터를 함께 삭제할 수 있도록 합니다. + * + * @property kafkaTemplate 전체 테이블 삭제 이벤트 발행용 KafkaTemplate + */ @Component class DeleteAllTableProducerImpl( private val kafkaTemplate: KafkaTemplate, ) : DeleteAllTableProducer { + /** + * 전체 테이블 삭제 이벤트를 Kafka로 발행합니다. + * + * DELETE_ALL_TABLE 토픽에 Unit 값을 전송하여 다른 서비스에서 + * 전체 데이터 삭제를 수행하도록 합니다. + */ override fun send() { kafkaTemplate.send(KafkaTopics.DELETE_ALL_TABLE, Unit) } diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteUserProducerImpl.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteUserProducerImpl.kt index 7610946..19a8686 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteUserProducerImpl.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/DeleteUserProducerImpl.kt @@ -4,12 +4,28 @@ import hs.kr.entrydsm.user.infrastructure.kafka.configuration.KafkaTopics import org.springframework.kafka.core.KafkaTemplate import org.springframework.stereotype.Component - +/** + * 사용자 삭제 이벤트를 발행하는 Producer 구현체입니다. + * + * 사용자 탈퇴 시 해당 사용자의 접수번호를 다른 마이크로서비스에 알려 + * 연관된 데이터를 함께 삭제할 수 있도록 합니다. + * + * @property kafkaTemplate 사용자 삭제 이벤트 발행용 KafkaTemplate + */ @Component class DeleteUserProducerImpl( private val kafkaTemplate: KafkaTemplate ) : DeleteUserProducer { + + /** + * 사용자 삭제 이벤트를 Kafka로 발행합니다. + * + * DELETE_USER 토픽에 접수번호를 전송하여 다른 서비스에서 + * 해당 접수번호와 연관된 데이터를 삭제하도록 합니다. + * + * @param receiptCode 삭제된 사용자의 접수번호 + */ override fun send(receiptCode: Long) { kafkaTemplate.send(KafkaTopics.DELETE_USER, receiptCode) } -} \ No newline at end of file +} diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducer.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducer.kt index 91543ed..f736b03 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducer.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducer.kt @@ -2,7 +2,28 @@ package hs.kr.entrydsm.user.infrastructure.kafka.producer import java.util.UUID +/** + * 사용자 이벤트를 발행하는 Producer 인터페이스입니다. + * + * 접수번호 업데이트 성공/실패 이벤트를 발행하여 + * Choreography 패턴 기반의 분산 트랜잭션을 지원합니다. + */ interface UserEventProducer { + + /** + * 접수번호 업데이트 완료 이벤트를 발행합니다. + * + * @param receiptCode 업데이트된 접수번호 + * @param userId 사용자 ID + */ fun sendReceiptCodeUpdateCompleted(receiptCode: Long, userId: UUID) + + /** + * 접수번호 업데이트 실패 이벤트를 발행합니다. + * + * @param receiptCode 업데이트 실패한 접수번호 + * @param userId 사용자 ID + * @param reason 실패 사유 + */ fun sendReceiptCodeUpdateFailed(receiptCode: Long, userId: UUID, reason: String) -} \ No newline at end of file +} diff --git a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducerImpl.kt b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducerImpl.kt index f68f3b2..f62f1da 100644 --- a/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducerImpl.kt +++ b/casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/producer/UserEventProducerImpl.kt @@ -7,19 +7,46 @@ import org.springframework.kafka.core.KafkaTemplate import org.springframework.stereotype.Service import java.util.UUID +/** + * 사용자 이벤트를 발행하는 Producer 구현체입니다. + * + * 접수번호 업데이트 성공/실패 결과를 다른 마이크로서비스에 알려 + * Choreography 패턴 기반의 분산 트랜잭션을 처리할 수 있도록 합니다. + * + * @property userEventKafkaTemplate 사용자 이벤트 발행용 KafkaTemplate + */ @Service class UserEventProducerImpl( private val userEventKafkaTemplate: KafkaTemplate, ): UserEventProducer { + /** + * 접수번호 업데이트 완료 이벤트를 발행합니다. + * + * 원서 서비스에서 요청한 접수번호 업데이트가 성공적으로 완료되었음을 + * 알리는 이벤트를 발행합니다. + * + * @param receiptCode 업데이트된 접수번호 + * @param userId 사용자 ID + */ override fun sendReceiptCodeUpdateCompleted(receiptCode: Long, userId: UUID) { val event = UserReceiptCodeUpdateCompletedEvent(receiptCode, userId) userEventKafkaTemplate.send(KafkaTopics.USER_RECEIPT_CODE_UPDATE_COMPLETED, event) } + /** + * 접수번호 업데이트 실패 이벤트를 발행합니다. + * + * 원서 서비스에서 요청한 접수번호 업데이트가 실패했음을 알려 + * 원서 서비스에서 보상 트랜잭션을 수행하도록 합니다. + * + * @param receiptCode 업데이트 실패한 접수번호 + * @param userId 사용자 ID + * @param reason 실패 사유 + */ override fun sendReceiptCodeUpdateFailed(receiptCode: Long, userId: UUID, reason: String) { val event = UserReceiptCodeUpdateFailedEvent(receiptCode, userId, reason) userEventKafkaTemplate.send(KafkaTopics.USER_RECEIPT_CODE_UPDATE_FAILED, event) } -} \ No newline at end of file +}