-
Notifications
You must be signed in to change notification settings - Fork 0
feature/33-kafka #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature/33-kafka #34
Changes from 18 commits
640fd30
34297ff
bc61ac3
b0e4cab
b2a419c
3aca7b3
1dcada7
0cd4320
996e4b6
f146848
b2797de
264a048
a89992b
9ab1874
d43a292
999fcad
21c5394
3e7c3e3
ca54f72
1b14e14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -22,4 +22,10 @@ interface DeleteUserPort { | |||||||||||||||||||||||||
| * @return 삭제 대상 사용자 목록 | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| fun findWithdrawnUsersOlderThan(days: Long): List<User> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * 모든 사용자를 삭제합니다. | ||||||||||||||||||||||||||
| * 관리자의 전체 데이터 초기화 시에만 사용됩니다. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| fun deleteAll() | ||||||||||||||||||||||||||
|
Comment on lines
+26
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 대량 삭제는 JPA deleteAll 대신 배치/DDL 기반으로 처리하세요. deleteAll은 엔티티 로딩/개별 삭제로 성능·락 부담이 큽니다. deleteAllInBatch/TRUNCATE(+ FK 안전성 검토)로 구현하고 반환값(삭제 건수)도 노출하는 편이 운영에 유리합니다. -interface DeleteUserPort {
+interface DeleteUserPort {
@@
- fun deleteAll()
+ fun deleteAll(): Long
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 트랜잭션 커밋 이전 성공 이벤트 발행 — 데이터/이벤트 불일치 위험 성공 이벤트를 DB 커밋 이전에 발행하고 있어, 커밋 실패 시 “성공 이벤트가 발행되었지만 데이터는 롤백된” 불일치가 발생할 수 있습니다. 커밋 이후(after-commit)에 이벤트를 발행하세요. Spring에서는 TransactionSynchronization 또는 도메인 이벤트 + 아래는 간단한 after-commit 발행 예시입니다. +import org.springframework.transaction.support.TransactionSynchronization
+import org.springframework.transaction.support.TransactionSynchronizationManager
...
- // 성공 이벤트 발행
- userEventProducer.sendReceiptCodeUpdateCompleted(receiptCode, userId)
+ // 커밋 이후 성공 이벤트 발행
+ TransactionSynchronizationManager.registerSynchronization(object : TransactionSynchronization {
+ override fun afterCommit() {
+ userEventProducer.sendReceiptCodeUpdateCompleted(receiptCode, userId)
+ }
+ })대안으로 Outbox 패턴도 고려해 주세요(신뢰성/재처리/순서 보장 용이). 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| } catch (e: Exception) { | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (e !is UserNotFoundException) { | ||||||||||||||||||||||||||||||||||||||||||
| userEventProducer.sendReceiptCodeUpdateFailed( | ||||||||||||||||||||||||||||||||||||||||||
| receiptCode = receiptCode, | ||||||||||||||||||||||||||||||||||||||||||
| userId = userId, | ||||||||||||||||||||||||||||||||||||||||||
| reason = e.message ?: "Unknown error" | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| throw e // 예외 다시 던져서 롤백 발생 | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<String, String> { | ||||||||||||||||||||||||||||||||||
| return ConcurrentKafkaListenerContainerFactory<String, String>().apply { | ||||||||||||||||||||||||||||||||||
| setConcurrency(2) | ||||||||||||||||||||||||||||||||||
| consumerFactory = DefaultKafkaConsumerFactory(consumerFactoryConfig()) | ||||||||||||||||||||||||||||||||||
| containerProperties.pollTimeout = 500 | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+35
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JsonDeserializer를 쓰는데 컨테이너 제네릭이 <String, String>인 불일치 — 런타임 캐스팅 이슈 위험 value를 JSON으로 역직렬화한다면 컨테이너/팩토리 제네릭을 <String, Any> 또는 구체 DTO로 맞추세요. 다음처럼 정렬하는 것을 권장합니다: - fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, String> {
- return ConcurrentKafkaListenerContainerFactory<String, String>().apply {
+ fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, Any> {
+ return ConcurrentKafkaListenerContainerFactory<String, Any>().apply {
setConcurrency(2)
- consumerFactory = DefaultKafkaConsumerFactory(consumerFactoryConfig())
+ consumerFactory = DefaultKafkaConsumerFactory<String, Any>(consumerFactoryConfig())
containerProperties.pollTimeout = 500
}
}필요 시 ack 모드도 명시적으로 설정해 주세요(예: RECORD/BATCH). +import org.springframework.kafka.listener.ContainerProperties
...
+ containerProperties.ackMode = ContainerProperties.AckMode.BATCH📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| private fun consumerFactoryConfig(): Map<String, Any> { | ||||||||||||||||||||||||||||||||||
| 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 "*", | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+59
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion max.poll.interval.ms=5000(5초)은 과도하게 낮아 잦은 리밸런스 유발 가능 처리 시간이 5초만 넘어도 세션이 끊길 수 있습니다. 기본값(5분, 300000) 수준으로 상향하거나 프로퍼티로 외부화하세요. - ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG to 5000,
+ ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG to 300_000,🤖 Prompt for AI AgentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JsonDeserializer TRUSTED_PACKAGES="*"는 보안 취약점 신뢰 패키지를 서비스 도메인으로 한정하세요. 필요 시 여러 패키지를 콤마로 나열. - JsonDeserializer.TRUSTED_PACKAGES to "*",
+ JsonDeserializer.TRUSTED_PACKAGES to "hs.kr.entrydsm.user.**,hs.kr.entrydsm.common.**",여러 이벤트 타입을 수신한다면 type headers를 유지하되, 기본 타입을 강제해야 할 경우: + JsonDeserializer.VALUE_DEFAULT_TYPE to "hs.kr.entrydsm.user.infrastructure.kafka.event.UserEvent",📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| "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}\";" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,60 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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<String, Unit> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return DefaultKafkaProducerFactory(producerConfig()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Bean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun deleteAllTableKafkaTemplate(): KafkaTemplate<String, Unit> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return KafkaTemplate(deleteAllTableProducerFactory()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+16
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainUnit 페이로드 전송은 직렬화/호환성 이슈 소지 — KafkaNull 또는 Void 사용 검토 Kotlin Unit은 Jackson/Spring JsonSerializer에서 의도치 않게 null/{}로 처리될 수 있습니다. 페이로드 미사용 시 KafkaNull 전송이 명확합니다. - fun deleteAllTableProducerFactory(): DefaultKafkaProducerFactory<String, Unit> {
- return DefaultKafkaProducerFactory(producerConfig())
+ fun deleteAllTableProducerFactory(): DefaultKafkaProducerFactory<String, Any> {
+ return DefaultKafkaProducerFactory(producerConfig())
}
...
- fun deleteAllTableKafkaTemplate(): KafkaTemplate<String, Unit> {
- return KafkaTemplate(deleteAllTableProducerFactory())
+ fun deleteAllTableKafkaTemplate(): KafkaTemplate<String, Any> {
+ return KafkaTemplate(deleteAllTableProducerFactory())
}사용부에서는 다음처럼 전송: import org.springframework.kafka.support.KafkaNull
kafkaTemplate.send(KafkaTopics.DELETE_ALL_TABLE, KafkaNull.INSTANCE)🏁 Script executed: #!/bin/bash
# Locate methods in KafkaProducerConfig.kt
rg -n "deleteAllTableProducerFactory" -C3 -t kt
rg -n "deleteAllTableKafkaTemplate" -C3 -t kt
rg -n "fun producerConfig" -C3 -t ktLength of output: 213 🏁 Script executed: #!/bin/bash
# KafkaProducerConfig.kt 파일 경로 찾기
file=$(find . -type f -name KafkaProducerConfig.kt)
# 해당 파일 내 함수 시그니처 및 producerConfig 정의 검색
rg -n "deleteAllTableProducerFactory" -C3 "$file"
rg -n "deleteAllTableKafkaTemplate" -C3 "$file"
rg -n "fun producerConfig" -C3 "$file"Length of output: 1417 🏁 Script executed: #!/bin/bash
file=./casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.kt
rg -n 'private fun producerConfig' -A10 "$file"Length of output: 1001 프로듀서 팩토리·템플릿 제네릭을 - fun deleteAllTableProducerFactory(): DefaultKafkaProducerFactory<String, Unit> {
+ fun deleteAllTableProducerFactory(): DefaultKafkaProducerFactory<String, Any> {
return DefaultKafkaProducerFactory(producerConfig())
}
- fun deleteAllTableKafkaTemplate(): KafkaTemplate<String, Unit> {
+ fun deleteAllTableKafkaTemplate(): KafkaTemplate<String, Any> {
return KafkaTemplate(deleteAllTableProducerFactory())
}이후 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Bean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun deleteUserProducerFactory(): DefaultKafkaProducerFactory<String, Long> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return DefaultKafkaProducerFactory(producerConfig()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Bean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun deleteUserKafkaTemplate(): KafkaTemplate<String, Long> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return KafkaTemplate(deleteUserProducerFactory()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Bean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun userEventProducerFactory(): DefaultKafkaProducerFactory<String, Any> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return DefaultKafkaProducerFactory(producerConfig()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Bean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun userEventKafkaTemplate(): KafkaTemplate<String, Any> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return KafkaTemplate(userEventProducerFactory()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private fun producerConfig(): Map<String, Any> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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}\";" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+97
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain보안 설정(프로토콜/메커니즘) 환경 불일치 가능 — TLS 기본화 권장 Confluent 환경이면 SASL_SSL/PLAIN 조합을, 자체 SCRAM 환경이어도 TLS 사용을 권장합니다. 현재 설정은 평문 전송입니다. - "security.protocol" to "SASL_PLAINTEXT",
- "sasl.mechanism" to "SCRAM-SHA-512",
- "sasl.jaas.config" to
- "org.apache.kafka.common.security.scram.ScramLoginModule required " +
+ "security.protocol" to "SASL_SSL",
+ "sasl.mechanism" to "PLAIN",
+ "sasl.jaas.config" to
+ "org.apache.kafka.common.security.plain.PlainLoginModule required " +
"username=\"${kafkaProperty.confluentApiKey}\" " +
"password=\"${kafkaProperty.confluentApiSecret}\";"환경에 맞는 truststore 설정도 점검하세요. 🏁 Script executed: #!/bin/bash
# KafkaProducerConfig.kt 파일 상단 컨텍스트 확인
sed -n '1,100p' casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaProducerConfig.ktLength of output: 2319 KafkaProducerConfig.kt – 보안 설정을 TLS 기반 SASL_SSL/PLAIN으로 변경 - "security.protocol" to "SASL_PLAINTEXT",
- "sasl.mechanism" to "SCRAM-SHA-512",
- "sasl.jaas.config" to
- "org.apache.kafka.common.security.scram.ScramLoginModule required " +
+ "security.protocol" to "SASL_SSL",
+ "sasl.mechanism" to "PLAIN",
+ "sasl.jaas.config" to
+ "org.apache.kafka.common.security.plain.PlainLoginModule required " +
"username=\"${kafkaProperty.confluentApiKey}\" " +
"password=\"${kafkaProperty.confluentApiSecret}\";"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+92
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 프로듀서 내구성/정확성 기본값 강화 필요(acks/idempotence 등) 운영 안전성을 위해 최소한 acks=all, enable.idempotence=true를 권장합니다. 배치/압축도 기본값을 튜닝하세요. 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,
+ ProducerConfig.ACKS_CONFIG to "all",
+ ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG to true,
+ ProducerConfig.RETRIES_CONFIG to Int.MAX_VALUE,
+ ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG to 120_000,
+ ProducerConfig.LINGER_MS_CONFIG to 10,
+ ProducerConfig.COMPRESSION_TYPE_CONFIG to "zstd",
"security.protocol" to "SASL_PLAINTEXT",
"sasl.mechanism" to "SCRAM-SHA-512",비고: COMPRESSION_TYPE은 인프라 표준에 맞춰 gzip/snappy/zstd 중 선택. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ConfigurationPropertiesBinding 사용이 부적절합니다. 해당 애노테이션은 바인딩 컨버터용입니다. 속성 클래스에는 제거하세요. (Spring Boot 3 기준) 또한 부트 버전에 따라 @ConstructorBinding 필요 여부가 다릅니다. -@ConfigurationPropertiesBinding
-@ConfigurationProperties("kafka")
-class KafkaProperty(
+@ConfigurationProperties("kafka")
+class KafkaProperty(
val serverAddress: String,
val confluentApiKey: String,
val confluentApiSecret: String
)부트 2.x(<=2.7) 사용 시: -@ConfigurationProperties("kafka")
-class KafkaProperty(
+@ConstructorBinding
+@ConfigurationProperties("kafka")
+class KafkaProperty(
val serverAddress: String,
val confluentApiKey: String,
val confluentApiSecret: String
)🤖 Prompt for AI Agents💡 Verification agent 🧩 Analysis chain부트 버전과 스캔 설정을 확인하세요. 부트 3+: @ConstructorBinding 불필요, @ConfigurationPropertiesScan 또는 @EnableConfigurationProperties 등록 필요. 현재 스캔 설정 유무를 점검해 주세요. 🏁 Script executed: #!/bin/bash
# 부트 버전 및 스캔 설정 확인
rg -n 'spring-boot-starter|spring-boot.gradle.plugin|org.springframework.boot' -C2 build.gradle* gradle/libs.versions.toml settings.gradle* || true
rg -n '@ConfigurationPropertiesScan|@EnableConfigurationProperties' -C3 --type=ktLength of output: 331 🏁 Script executed: #!/bin/bash
# 스프링 부트 플러그인 버전 확인
echo ">>> build.gradle.kts:"
sed -n '1,50p' build.gradle.kts || echo "build.gradle.kts 파일을 찾을 수 없습니다."
echo
echo ">>> settings.gradle.kts:"
sed -n '1,50p' settings.gradle.kts || echo "settings.gradle.kts 파일을 찾을 수 없습니다."
echo
# Spring Boot 플러그인 ID 검색
rg -n 'id\("org.springframework.boot"\)' -g '*.kts'
echo
# 메인 애플리케이션 클래스 확인
rg -n '@SpringBootApplication' -C2 -g '*.kt'
echo
# @ConfigurationPropertiesScan 또는 @EnableConfigurationProperties 사용 여부 확인
rg -n '@ConfigurationPropertiesScan|@EnableConfigurationProperties' -C3 -g '*.kt'Length of output: 2870 🏁 Script executed: #!/bin/bash
# Subproject의 build.gradle.kts 확인
echo ">>> casper-user/build.gradle.kts:"
sed -n '1,50p' casper-user/build.gradle.kts || echo "Not found: casper-user/build.gradle.kts"
echo
# Spring Boot Gradle 플러그인 적용 여부 검색 (플러그인 DSL)
rg -n 'id\s*\(\s*["'\'']org.springframework.boot["'\'']\s*\)' -g 'casper-user/build.gradle.kts' || echo "No org.springframework.boot plugin DSL found"
echo
# Spring Boot 플러그인 적용 여부 검색 (apply 방식)
rg -n 'apply\s*\(\s*["'\'']org.springframework.boot["'\'']\s*\)' -g 'casper-user/build.gradle.kts' || echo "No apply(\"org.springframework.boot\") usage found"
echo
# 의존관계에서 Spring Boot 스타터 버전 확인
echo ">>> dependencies in casper-user/build.gradle.kts:"
sed -n '/dependencies\s*{/,/}/p' casper-user/build.gradle.ktsLength of output: 3927 🏁 Script executed: #!/bin/bash
# casper-convention 모듈에서 SPRING_BOOT_VERSION 상수 정의 확인
rg -n "SPRING_BOOT_VERSION" -C3 casper-convention
# PluginVersion 객체 전체 검색
rg -n "object PluginVersion" -C5 casper-conventionLength of output: 102 @ConfigurationPropertiesBinding 어노테이션 제거 및 Spring Boot 버전 확인
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| 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" | ||
|
|
||
| // 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" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
|
Comment on lines
+15
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain소비 실패 시 재시도/치명적 메시지 처리(DLT)와 로깅 부족 JSON 역직렬화나 도메인 처리 실패가 발생하면 컨테이너의 기본 에러 핸들러 설정에 의존하게 됩니다. 재시도/백오프/Dead Letter Topic(DLT) 정책이 없다면 동일 메시지의 무한 재처리 또는 조용한 드롭이 발생할 수 있습니다. 최소한 로깅과 재시도/복구 전략을 명시해 주세요. 아래처럼 +import org.slf4j.LoggerFactory
...
) {
+ private val log = LoggerFactory.getLogger(CreateApplicationConsumer::class.java)
...
- fun execute(message: String) {
- val createApplicationEvent = mapper.readValue(message, CreateApplicationEvent::class.java)
- changeReceiptCodeUseCase.changeReceiptCode(createApplicationEvent.userId, createApplicationEvent.receiptCode)
- }
+ fun execute(message: String) {
+ runCatching { mapper.readValue(message, CreateApplicationEvent::class.java) }
+ .onSuccess { event ->
+ changeReceiptCodeUseCase.changeReceiptCode(event.userId, event.receiptCode)
+ }
+ .onFailure { ex ->
+ log.warn("CREATE_APPLICATION 소비 실패: {}", ex.message, ex)
+ throw ex
+ }
+ }DLT/에러핸들러 구성이 있는지 확인해 주세요. 없는 경우 🏁 Script executed: #!/bin/bash
# DLT/에러핸들러/RetryableTopic 구성 존재 여부 점검
rg -nP -C2 '(DefaultErrorHandler|CommonErrorHandler|DeadLetterPublishingRecoverer|@RetryableTopic|kafkaListenerContainerFactory)' --type=ktLength of output: 168 🏁 Script executed: #!/bin/bash
# Kotlin 파일에서 Retry/DLT/ErrorHandler 구성 존재 여부 재확인
rg -nP -C2 '(DefaultErrorHandler|CommonErrorHandler|DeadLetterPublishingRecoverer|@RetryableTopic|kafkaListenerContainerFactory)' -g '*.kt'Length of output: 2385 🏁 Script executed: #!/bin/bash
# KafkaConsumerConfig 파일 내용 출력
sed -n '1,200p' casper-user/src/main/kotlin/hs/kr/entrydsm/user/infrastructure/kafka/configuration/KafkaConsumerConfig.ktLength of output: 2219 CreateApplicationConsumer execute 메서드에 예외 처리 및 재시도/DLT 구성 누락 🤖 Prompt for AI Agents |
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
|
Comment on lines
+26
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain재처리/중복 처리 안전성 확인
다음 검색으로 설정을 빠르게 점검할 수 있습니다. 🏁 Script executed: #!/bin/bash
rg -nP 'class\s+KafkaConsumerConfig|ConcurrentKafkaListenerContainerFactory|CommonErrorHandler|DefaultErrorHandler|AckMode' -C3Length of output: 3036 컨슈머 에러 핸들러·DLT 구성 및 동시성 설정 검토
|
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<String, Unit>, | ||||||||||||||||||||||||||||||||
| ) : DeleteAllTableProducer { | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| override fun send() { | ||||||||||||||||||||||||||||||||
| kafkaTemplate.send(KafkaTopics.DELETE_ALL_TABLE, Unit) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unit 페이로드 직렬화 실패 가능성 높음: 값 타입을 명시적 DTO/문자열로 교체
-class DeleteAllTableProducerImpl(
- private val kafkaTemplate: KafkaTemplate<String, Unit>,
-) : DeleteAllTableProducer {
+class DeleteAllTableProducerImpl(
+ private val kafkaTemplate: KafkaTemplate<String, String>,
+) : DeleteAllTableProducer {
override fun send() {
- kafkaTemplate.send(KafkaTopics.DELETE_ALL_TABLE, Unit)
+ kafkaTemplate.send(KafkaTopics.DELETE_ALL_TABLE, "{}")
}
}장기적으로는 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package hs.kr.entrydsm.user.infrastructure.kafka.producer | ||
|
|
||
| import hs.kr.entrydsm.user.infrastructure.kafka.configuration.KafkaTopics | ||
| import org.springframework.kafka.core.KafkaTemplate | ||
| import org.springframework.stereotype.Component | ||
|
|
||
|
|
||
| @Component | ||
| class DeleteUserProducerImpl( | ||
| private val kafkaTemplate: KafkaTemplate<String, Long> | ||
| ) : DeleteUserProducer { | ||
| override fun send(receiptCode: Long) { | ||
| kafkaTemplate.send(KafkaTopics.DELETE_USER, receiptCode) | ||
| } | ||
| } |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
spring-cloud-starter-config → spring-cloud-starter 변경은 오타 가능성이 큽니다
Config 클라이언트를 쓰려면
spring-cloud-starter-config가 맞습니다.spring-cloud-starter는 기능 제공용 스타터가 아니고, 버전 지정도spring-cloud-dependenciesBOM으로 관리하는 편이 안전합니다. 되돌리는 것을 권장합니다.📝 Committable suggestion
🤖 Prompt for AI Agents