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:
rtgs
2026-07-15 15:37:32 +09:00
commit 58ca23b5d9
140 changed files with 11832 additions and 0 deletions

View 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")
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View 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