Initialize RTGS prototype repository
3센터 A-A-A RTGS 실시간총액결제 프로토타입 최초 버전관리 시작. - backend: Kotlin/Gradle 멀티모듈(sequencer, common, 채널/센터 모듈) - frontend: Vite + TS - infra: docker-compose, prometheus/grafana, ELK 네이티브 스크립트 - loadtest(k6), sim(장애/순서/정합성 시나리오), docs(PoC 보고서/복원력방안)
This commit is contained in:
20
backend/dior/build.gradle.kts
Normal file
20
backend/dior/build.gradle.kts
Normal file
@@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.spring")
|
||||
id("org.springframework.boot")
|
||||
id("io.spring.dependency-management")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation("org.springframework.boot:spring-boot-starter")
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web") // 메트릭/헬스 노출(B4)
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.kafka:spring-kafka")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package kr.or.bok.rtgs.dior
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import kr.or.bok.rtgs.common.ledger.TransferRecord
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Modifying
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
|
||||
import org.springframework.data.repository.query.Param
|
||||
|
||||
@SpringBootApplication
|
||||
@EntityScan("kr.or.bok.rtgs.common.ledger")
|
||||
@EnableJpaRepositories(considerNestedRepositories = true)
|
||||
class DiorApplication {
|
||||
@Bean
|
||||
fun objectMapper(): ObjectMapper = jacksonObjectMapper()
|
||||
}
|
||||
|
||||
interface TransferRepository : JpaRepository<TransferRecord, String> {
|
||||
/**
|
||||
* 접수내역을 PDNG로 삽입하되, 이미 존재하면(=Hermes/타 소비자가 먼저 만든 경우) 아무것도 안 함.
|
||||
* 원자적 upsert라 Dior-Hermes 동시 삽입 경합에서 중복키 예외가 발생하지 않는다.
|
||||
*/
|
||||
@Modifying
|
||||
@Query(
|
||||
value = """
|
||||
INSERT INTO transfer
|
||||
(bmi, global_seq, status, sender_code, receiver_code, amount, origin_center, created_at, updated_at)
|
||||
VALUES
|
||||
(:bmi, :seq, 'PDNG', :sender, :receiver, :amount, :origin, :createdAt, :updatedAt)
|
||||
ON CONFLICT (bmi) DO NOTHING
|
||||
""",
|
||||
nativeQuery = true,
|
||||
)
|
||||
fun insertPdngIfAbsent(
|
||||
@Param("bmi") bmi: String,
|
||||
@Param("seq") seq: Long,
|
||||
@Param("sender") sender: String,
|
||||
@Param("receiver") receiver: String,
|
||||
@Param("amount") amount: Long,
|
||||
@Param("origin") origin: String,
|
||||
@Param("createdAt") createdAt: Long,
|
||||
@Param("updatedAt") updatedAt: Long,
|
||||
): Int
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<DiorApplication>(*args)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package kr.or.bok.rtgs.dior
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.or.bok.rtgs.common.JournalEntry
|
||||
import kr.or.bok.rtgs.common.Topics
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.kafka.annotation.KafkaListener
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
/**
|
||||
* DIOR(접수동기화) — 저널(rtgs.journal)을 구독해 접수 내역을 권위 원장(PostgreSQL)에
|
||||
* 대기상태(PDNG)로 영구 저장한다. BMI 중복체크로 멱등성 보장(센터 간 동일 저널 재생).
|
||||
*/
|
||||
@Service
|
||||
class DiorService(
|
||||
private val transfers: TransferRepository,
|
||||
private val mapper: ObjectMapper,
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
// 센터별 그룹(dior-DC1/DC2/DC3): 각 센터가 저널 전량을 독립 재생.
|
||||
@KafkaListener(topics = [Topics.JOURNAL], groupId = "dior-\${rtgs.center-id:DC1}", concurrency = "1")
|
||||
@Transactional
|
||||
fun onJournal(payload: String) {
|
||||
val e = mapper.readValue(payload, JournalEntry::class.java)
|
||||
// 원자적 upsert: 이미 있으면(Hermes/타 소비자가 먼저 만든 경우) 아무 일도 안 함 → 중복키 예외 없음.
|
||||
val inserted = transfers.insertPdngIfAbsent(
|
||||
bmi = e.core.bmi,
|
||||
seq = e.globalSeq,
|
||||
sender = e.core.senderCode,
|
||||
receiver = e.core.receiverCode,
|
||||
amount = e.core.amount,
|
||||
origin = e.originCenter,
|
||||
createdAt = e.seqEpochMillis,
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
)
|
||||
if (inserted > 0) log.info("PDNG seq=#{} bmi={}", e.globalSeq, e.core.bmi)
|
||||
else log.debug("PDNG skip(exists) seq=#{} bmi={}", e.globalSeq, e.core.bmi)
|
||||
}
|
||||
}
|
||||
30
backend/dior/src/main/resources/application.yml
Normal file
30
backend/dior/src/main/resources/application.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
spring:
|
||||
application:
|
||||
name: dior
|
||||
datasource:
|
||||
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5433}/${POSTGRES_DB:rtgs}
|
||||
username: ${POSTGRES_USER:rtgs}
|
||||
password: ${POSTGRES_PASSWORD:rtgs}
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
open-in-view: false
|
||||
kafka:
|
||||
bootstrap-servers: ${KAFKA_BOOTSTRAP:localhost:9092}
|
||||
consumer:
|
||||
group-id: dior
|
||||
auto-offset-reset: earliest
|
||||
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
|
||||
server:
|
||||
port: 8092
|
||||
|
||||
rtgs:
|
||||
center-id: ${CENTER_ID:DC1}
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,prometheus,metrics
|
||||
Reference in New Issue
Block a user