๐ง ATMS - ELK Stack ๊ตฌ์ถ(Kafka ๊ธฐ๋ฐ ์ ๋ ์คํธ๋ฆฌ๋ฐ)
์ง๋ ๊ธ ๋ง๋ฏธ์ ๋ค์์ ๋ชจ๋ํฐ๋ง(Prometheus + Grafana) ์ด์ผ๊ธฐ๋ฅผ ํ๊ฒ ๋ค๊ณ ์๊ณ ํ์๋๋ฐ, ๊ทธ ์ ์ ์ต๊ทผ ์์ ํ ์ ์์ ๋ ์ค์๊ฐ ๊ฒ์ ํ์ดํ๋ผ์ธ ๊ตฌ์ถ์ด ๋จผ์ ๋ง๋ฌด๋ฆฌ๋ผ์ ์ด ๊ธ๋ถํฐ ์ ๋ฆฌํ๋ค.
ATM ๊ธฐ๊ธฐ(5,500๋)๊ฐ ๋ณด๋ด๋ ์ ์์ ๋(๊ฑฐ๋/์ํ/์ฅ์ /๋ง๊ฐ/์์ฌ ๋ฑ)์ ์๋๋ Oracle DB(T_JOURNAL, T_JOURNAL_DATA)์ ์ ์์ด๊ณ ์์๋ค. ๋ฌธ์ ๋ ์ด์ ํํฉ์ ์ค์๊ฐ์ผ๋ก ๋ค์ฌ๋ค๋ณผ ๋ฐฉ๋ฒ์ด SQL ์กฐํ๋ฟ์ด์๋ค๋ ๊ฒ โ ํน์ ๊ธฐ๊ธฐ๊ฐ ์ธ์ ๋ถํฐ ์์ ์ด ๋๊ฒผ๋์ง, ์ฅ์ ์ ๋์ด ์ด๋ ๊ธฐ๊ธฐ์ ๋ชฐ๋ฆฌ๋์ง๋ฅผ ๋ณด๋ ค๋ฉด ๋งค๋ฒ ์ฟผ๋ฆฌ๋ฅผ ์ง์ผ ํ๋ค.
๊ทธ๋์ ์ ๋์ด ๋ค์ด์ค๋ ์ฆ์ ๊ฒ์ ๊ฐ๋ฅํ ํํ๋ก ์์ธํ๊ณ , ์ด์์๊ฐ ๋ณผ ์ ์๋ ๋์๋ณด๋๊น์ง ๋ถ์ด๊ธฐ๋ก ํ๋ค.
"๊ฒ์ ๊ฐ๋ฅํ๊ฒ ๋ง๋ค์"๊น์ง๋ ์ฌ์ ์ง๋ง ๊ทธ ๋ค์์ด ๊ฐ๋ฆผ๊ธธ์ด์๋ค. ๋ก๊ทธ ๊ธฐ๋ฐ(Logstash๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ๋ก๊ทธ๋ฅผ tail)์ผ๋ก ๊ฐ์ง, ์ด๋ฒคํธ ๊ธฐ๋ฐ(์ ํ๋ฆฌ์ผ์ด์ ์ด ์ง์ ๋ฐํ)์ผ๋ก ๊ฐ์ง โ ๊ฐ ๋ฐฉ์์ ํธ๋ ์ด๋์คํ๋ฅผ Claude Code์ ํ๋์ฉ ์ง์ด๋ณด๊ณ , ์ด ํ๋ก์ ํธ์ ์ ์ฝ ์กฐ๊ฑด์ ๋ง์ถฐ ๊ณจ๋๋ค.
์ ์ฝ ์กฐ๊ฑด์ ์ธ ๊ฐ์ง์๋ค.
| ๊ตฌ๋ถ | ๋ก๊ทธ ๊ธฐ๋ฐ (Logstash๊ฐ ๋ก๊ทธ tail) | ์ด๋ฒคํธ ๊ธฐ๋ฐ (์ด๋ฒ ์ ํ) |
|---|---|---|
| ์๋ณธ ์์น | ๋ก๊ทธ ํ์ผ(ํ์ ยท์ ์ค ์ํ) | DB(ํญ์ ์๋ณธ) |
| ๋ฐ์ดํฐ ๊ตฌ์กฐ | ํ ์คํธ ํ์ฑ ํ์ | ์ด๋ฏธ ๊ตฌ์กฐํ๋ JSON |
| ๊ธฐ๊ธฐ๋ณ ์์ ๋ณด์ฅ | ์ฌ์ค์ ์ด๋ ค์ | Kafka ํํฐ์ ํค๋ก ๋ณด์ฅ |
| ์์ ๊ฒฝ๋ก ์ํฅ | ๊ฑฐ์ ์์(์์ ๋น๋๊ธฐ tail) | ์์ โ ๋ฐํ ์ฝ๋๊ฐ ์์ ๊ฒฝ๋ก์ ์ถ๊ฐ๋จ |
๋ก๊ทธ ๊ธฐ๋ฐ์ด "์์ ๊ฒฝ๋ก ๋ฌด์ํฅ" ํญ๋ชฉ์์๋ ๋ ์ ๋ฆฌํ๋ค. ๊ทธ๋ฐ๋ฐ๋ ์ด๋ฒคํธ ๊ธฐ๋ฐ์ ํํ ์ด์ ๋, ์ ๋์ด ์ด๋ฏธ ๊ตฌ์กฐํ๋ ๋น์ฆ๋์ค ์ด๋ฒคํธ์ด์ง ํ ์คํธ ๋ก๊ทธ๊ฐ ์๋์๊ธฐ ๋๋ฌธ์ด๋ค โ ๋ก๊ทธ๋ก ํ๋ฒ ํ์ด์ผ๋ค๊ฐ Logstash๋ก ๋ค์ ํ์ฑํ๋ ๊ฑด ์ฐํ์๋ค.
๋์ ์ด๋ฒคํธ ๊ธฐ๋ฐ์ ์ ์ผํ ์ฝ์ ("์์ ๊ฒฝ๋ก ์ํฅ")์ ๋ค์ ์ค๊ณ๋ก ์ ๋ฉด ๋์ํ๊ธฐ๋ก ํ๋ค.
์์น์ "๋ถ๊ฐ ๊ธฐ๋ฅ์ด ํต์ฌ ๊ธฐ๋ฅ์ ์ ๋ ๊ฑด๋๋ฆฌ๋ฉด ์ ๋๋ค"๋ ๊ฒ. ์ด๊ฑธ ๋ฌธ์๋ก๋ง ๋จ๊ธฐ์ง ์๊ณ ๋ค ๊ฐ์ง ์ฅ์น๋ก ์ฝ๋์ ๋ฐ์ ๋ฃ์๋ค.
โ ์์ opt-in โ ํ๋๊ทธ ๋ ๊ฐ, ๊ธฐ๋ณธ๊ฐ ์ ๋ถ false
@ConfigurationProperties(prefix = "atms.stream")
public class JournalStreamProperties {
private final Kafka kafka = new Kafka(); // kafka.enabled = false (๊ธฐ๋ณธ)
private final Elasticsearch elasticsearch = new Elasticsearch(); // enabled = false (๊ธฐ๋ณธ)
}
โก ๊ด๋ จ ์ปดํฌ๋ํธ ์ ๋ถ @ConditionalOnProperty๋ก ๊ฒ์ดํ
ํ ํฝ ์์ฑ(JournalStreamTopicConfig), ์ธ๋ฑ์ค ํ
ํ๋ฆฟ ๋ฑ๋ก(JournalEsIndexInitializer), ์ปจ์๋จธ(JournalEsIndexConsumer) โ ์
๋ค ํ๋๊ทธ๊ฐ ๊บผ์ ธ ์์ผ๋ฉด ์คํ๋ง ์ปจํ
์คํธ์ ์์ ์กด์ฌํ์ง ์๋๋ค.
โข DB ์ปค๋ฐ ์ดํ์๋ง ๋ฐํ, ๊ทธ๋ฆฌ๊ณ ์คํจํด๋ ์์ธ๋ฅผ ๋์ง์ง ์๋๋ค
// JournalDispatchService
txRequired.executeWithoutResult(tx -> doDispatchInTx(key, msg));
// DB ์ ์ฌ ์ฑ๊ณต ํ์๋ง ์คํธ๋ฆผ ๋ฐํ(ES ์์ธ์ฉ) โ ๋ฐํ ์คํจ๋ ์์ ์๋ต์ ์ํฅ ์์
eventPublisher.publishStored(key, msg);
// JournalEventPublisher.publishStored()
if (!props.getKafka().isEnabled()) return; // ๊บผ์ ธ ์์ผ๋ฉด ์ฆ์ no-op
...
template.send(topic, kafkaKey, payload)
.whenComplete((result, ex) -> {
if (ex != null) log.warn("journal event publish failed ...: {}", ex.toString());
// ์์ธ๋ฅผ ์ฌ๊ธฐ์ ์ ๋ ๋ค์ ๋์ง์ง ์๋๋ค
});
โฃ ES๊ฐ ์ฃฝ์ด์์ด๋ ์ฑ ๊ธฐ๋์ ๋ง์ง ์๋๋ค
JournalEsIndexInitializer๋ ๊ธฐ๋ ์ ์ธ๋ฑ์ค ํ
ํ๋ฆฟ์ ๋ฑ๋กํ๋๋ฐ, ES๊ฐ ์์ง ์ ๋ ์์ผ๋ฉด ๊ฒฝ๊ณ ๋ก๊ทธ๋ง ๋จ๊ธฐ๊ณ ๊ทธ๋ฅ ๋์ด๊ฐ๋ค (์์ธ ์์ ์ ๋์ ๋งคํ์ผ๋ก ๋์ฒด ๋์).
Claude Code์ ์ด ๋ถ๋ถ์ ๊ฒํ ํ๋ฉด์ ๋์จ ์๊ธฐ๋ค. try-catch๋ก ๊ฐ์ธ๋ ๊ฒ๊ณผ, ์ปดํฌ๋ํธ ์์ฒด๋ฅผ ์กฐ๊ฑด๋ถ๋ก ์กด์ฌํ์ง ์๊ฒ ๋ง๋๋ ๊ฒ์ ๋ค๋ฅด๋ค. ํ์๋ "์ด ๊ธฐ๋ฅ์ ์ผ ์ ์ด ์๋ ํ๊ฒฝ"์์๋ ๊ด๋ จ ๋น์ด ์์ ์์ฑ๋์ง ์์ผ๋, ์ฝ๋ ๋ฆฌ๋ทฐ ์์ ์๋ "์ด ๋ฐฐํฌ์ ์ด ๊ธฐ๋ฅ์ด ๊ด์ฌํ๋๊ฐ"๊ฐ ์ค์ ํ๋๋ก ๋ช ํํด์ง๋ค.
ATM ์์ด์ ํธ๋ ์๋ต์ ๋ชป ๋ฐ์ผ๋ฉด ๊ฐ์ ์ ๋์ ์ฌ์ ์กํ๋ค. Kafka ์ปจ์๋จธ๊ฐ ๊ฐ์ ๋ฉ์์ง๋ฅผ ๋ ๋ฒ ์ฒ๋ฆฌํ ๊ฐ๋ฅ์ฑ๋ ์๋ค. ๊ทธ๋์ ์์ธ ์์ ์ ๊ฒฐ์ ๋ก ์ ๋ฌธ์ ID๋ฅผ ๋ง๋ค๊ณ , POST๊ฐ ์๋ PUT์ผ๋ก ์์ธํ๋๋ก ํ๋ค.
// JournalEsIndexConsumer
String docId = String.join("-",
termGroupId, termId, jnlDate, jnlTime, jnlSeq);
Request request = new Request("PUT", "/" + indexName(jnlDate) + "/_doc/" + docId);
๊ฐ์ ์ ๋์ด ๋ช ๋ฒ์ ๋ค์ ๋ค์ด์๋ ๋์ฐฉํ๋ ๋ฌธ์ ID๋ ํญ์ ๋์ผํ๋, ์ค๋ณต ์์ธ์ด ์๋๋ผ ๊ฐ์ ๋ฌธ์๋ฅผ ๋ฎ์ด์ฐ๋ ๊ฒ์ผ๋ก ๋๋๋ค. ์ธ๋ฑ์ค ์ด๋ฆ๋ "์ค๋ ๋ ์ง"๊ฐ ์๋๋ผ ์ ๋ ์์ฒด์ ๊ฑฐ๋์ผ์(jnlDate)๋ก ์ ํด์, ๋ค๋ฆ๊ฒ ๋ค์ด์จ ์ง๋ ์ ๋๋ ์๋ฑํ ๋ ์ง์ ์ธ๋ฑ์ค์ ์์ด์ง ์๋๋ค.
ํ๋ ๋งคํ์ ์ ๋ ์ข
๋ฅ(๊ธฐ๋/๊ฐ๊ตญ/์ํ/์ฅ์ /๋ง๊ฐ/์์ฌ/๊ฑฐ๋ โ ์ด 7์ข
)๋ง๋ค ๊ตฌ์กฐ๊ฐ ๋ฌ๋ผ์, head/body๋ฅผ Elasticsearch์ flattened ํ์
์ผ๋ก ๋งคํํด ์คํค๋ง ๊ฑฑ์ ์์ด ํต์งธ๋ก ์์ธํ๊ณ , ์ ์ฒด payload๋ searchText(text ํ์
)์ ์ค๋ณต ์ ์ฅํด Kibana์์ ๊ณ์ขยท์๋ฌ์ฝ๋ ์ ๋ฌธ๊ฒ์์ด ๊ฐ๋ฅํ๊ฒ ํ๋ค.
graph TD
A[ATM Agent 5,500๋]
B[atms-journal ์์ API]
C[Oracle DB ์ ์ฌ - T_JOURNAL]
D[JournalEventPublisher - fire-and-forget]
E[Kafka topic atms.journal.received - ํํฐ์
12]
F[JournalEsIndexConsumer]
G[Elasticsearch ์ผ๋ณ ์ธ๋ฑ์ค]
H[Kibana ๋์๋ณด๋]
A --> B --> C --> D --> E --> F --> G --> H
termGroupId:termId โ ๊ฐ์ ๊ธฐ๊ธฐ์ ์ ๋์ ํญ์ ๊ฐ์ ํํฐ์
์ผ๋ก ๊ฐ์ ์์๊ฐ ๋ณด์ฅ๋๋ค.๊ฐ๋ฐ ํ๊ฒฝ์ Docker Compose ํ ๋ฐฉ์ผ๋ก ๋์ด๋ค.
docker compose up -d kafka elasticsearch kibana atms-journal
./infra/elastic/setup-kibana.sh # ๋ฐ์ดํฐ๋ทฐ(atms-journal-*) ๋ฑ๋ก
Kafka๋ Zookeeper ์๋ KRaft ๋จ์ผ ๋ ธ๋(3.9.1), ES/Kibana๋ 8.18.2. ์ ๋ถ ๊ฐ๋ฐ์ฉ ๋จ์ผ ๋ ธ๋ ๊ตฌ์ฑ์ด๊ณ , ์ด์ ์ ํ ์์๋ Kafka 3๋ ธ๋+SASL/TLS, ES 3๋ ธ๋+๋ณด์ ํ์ฑํ, ๊ณ์ข๋ฒํธ ๋ฑ ๊ฐ์ธ์ ๋ณด ๋ง์คํน ํ ์์ธ, ์๋ ์ญ์ ๋์ ILM ์ ์ฑ ์ ์ฉ์ด ํ์ํ๋ค โ ์ฌ๊ธฐ๊น์ง ์์ง TODO๋ก ๋จ์์๋ค.
์ค๊ณ๋ง ๊ทธ๋ด๋ฏํ ๊ฒ๊ณผ ์ค์ ๋ก ๋ถํ๋ฅผ ๊ฒฌ๋๋ ๊ฒ์ ๋ค๋ฅธ ๋ฌธ์ ๋ผ, ์์ด์ ํธ ํ๋กํ ์ฝ์ ๊ทธ๋๋ก ํ๋ด ๋ธ ๋ถํ ํ ์คํธ ์คํฌ๋ฆฝํธ๋ก ํผํฌ ์๋๋ฆฌ์ค(550๋ ร ๊ธฐ๊ธฐ๋น 10๊ฑด = 5,500๊ฑด, ๋ณ๋ ฌ 32)๋ฅผ ๋ ๋ฒ ๋๋ ธ๋ค.
| ์์ | ํ๊ฒฝ | ๊ฒฐ๊ณผ |
|---|---|---|
| 1์ฐจ | ๋ก์ปฌ, Kafka/ES off | 5,500/5,500 ์ฑ๊ณต, 42์ด, TPS 131๊ฑด/์ด (๋ชฉํ ํํ 100/s ์ํ) |
| 2์ฐจ | ๋ฐฐํฌ ์๋ฒ, Kafka/ES on | 5,500/5,500 ์ฑ๊ณต, 16์ด, TPS 343๊ฑด/์ด (๋ชฉํ ์ํ 300/s๋ ์ํ) |
2์ฐจ ํ ์คํธ์์ KafkaโES ์์ธ๊น์ง 5,501/5,501 ๋ฌด์์ค, dispatch ์คํจยทpublish ์คํจ ๋ชจ๋ 0๊ฑด.
Kafka/ES๋ฅผ ์ผฐ๋๋ฐ ์คํ๋ ค ๋ ๋นจ๋ผ์ง ๊ฑด, ๋ ์ฐจ๋ก ํ ์คํธ ์ฌ์ด์ ๋ฐฐ์น ์ฒ๋ฆฌ ๊ฒฝ๋ก ์์ฒด๋ ํจ๊ป ์๋ดค๊ธฐ ๋๋ฌธ์ด๋ค โ ์คํธ๋ฆฌ๋ฐ ํ์ดํ๋ผ์ธ์ด "์นํ์" ๋๋ ค์ง์ง ์์๋ค๋ ๊ฒ ์ด๋ฒ ๊ฒ์ฆ์ ํต์ฌ์ด์๋ค.
์ฑ๋ฅ ์์น๋ณด๋ค ๋ถํ ํ ์คํธ ์์ฒด๊ฐ ์ก์๋ธ ๊ฒฐํจ์ด ๋ ์ ์ฉํ๋ค.
โ CASSETTE ์ํ MERGE๊ฐ ๋ ๋ฒ์งธ ์ ๋๋ถํฐ ์คํจ
ORA-00001: unique constraint violated
๊ฐ์ ๊ธฐ๊ธฐ์ ์์ฌ/๊ฑฐ๋ ์ ๋์ด ๋ ๋ฒ์งธ๋ก ๋ค์ด์ค๋ ์๊ฐ๋ถํฐ ์คํจํ๋ค. ์์ธ์ MERGE ๋ฌธ์ ๋งค์นญ ์กฐ๊ฑด โ DB ์ปฌ๋ผ์ CHAR(5)(๊ณต๋ฐฑ ํจ๋ฉ)์ธ๋ฐ ๋ฐ์ธ๋ ๊ฐ์ VARCHAR๋ผ ํญ์ NOT MATCHED๋ก ํ์ ๋์ด, ๋งค๋ฒ ์๋ก INSERT๋ฅผ ์๋ํ๋ค PK ์ถฉ๋์ด ๋ ๊ฒ.
-- ์์ ์ : CHAR(5) vs VARCHAR ๋น๊ต๋ผ ๋งค๋ฒ ๋ถ์ผ์น
ON (T.CASSETTE_NO = :cassetteNo)
-- ์์ ํ: ๋ช
์์ ์ผ๋ก CHAR(5)๋ก ์บ์คํ
ํด ๋น๊ต
ON (T.CASSETTE_NO = CAST(:cassetteNo AS CHAR(5)))
โก ๊ฑฐ๋์ ๋ ์ํ์ฝ๋ ์๋ฆฌ์ ๋ถ์ผ์น๋ก ์ธํ ์ค๋ฒํ๋ก
์ ๋ ์คํ์ ์ํ์ฝ๋๋ 4์๋ฆฌ์ธ๋ฐ, ์ ์ DB ์ปฌ๋ผ์ 3์๋ฆฌ๋ก ์ ์๋ผ ์์ด ๊ฐ์ด ์๋ ค ๋ค์ด๊ฐ๋ ๋ฌธ์ ๊ฐ ์์๋ค. ์คํ๊ณผ ์คํค๋ง ์ค ์ด๋ ์ชฝ์ด ๋ง๋์ง๋ ์ค๊ณ ๊ฒ์ฆ์ด ๋ ํ์ํ ๋ถ๋ถ์ด๋ผ, ์ฐ์ ๋ค 3์๋ฆฌ๋ง ์ทจํ๋ ์ ๊ทํ ํจ์๋ฅผ ์ถ๊ฐํด ์ค๋ฒํ๋ก๋ถํฐ ๋ง์๋ค.
private static String bank3(String bkCd) {
// ์คํ(4์๋ฆฌ) โ ์ปฌ๋ผ(3์๋ฆฌ) ์ ๊ทํ. ์: "1234" โ "234"
return (bkCd != null && bkCd.length() == 4) ? bkCd.substring(1) : bkCd;
}
๋ ๋ค ํ์ ํธ๋ํฝ์์๋ ์ ๋๋ฌ๋๋ค๊ฐ, ๋ถํ ํ ์คํธ๋ก "๊ฐ์ ๊ธฐ๊ธฐ๊ฐ ๋ฐ๋ณตํด์ ์ ๋์ ๋ณด๋ด๋" ์ํฉ์ ์ฌํํ๊ณ ๋์์ผ ๋์จ ๊ฒฐํจ์ด์๋ค.
๊ฒฐ๋ก
Kibana ๋์๋ณด๋๋ฅผ ์ฝ๋(PowerShell ์คํฌ๋ฆฝํธ)๋ก ์กฐ๋ฆฝํด ๋ฐฐํฌํ๋ ์ด์ผ๊ธฐ๋ ๋ถ๋์ ์ด๋ฒ ๊ธ์์๋ ๋ค๋ฃจ์ง ๋ชปํ๋ค โ ๋ค์ ๊ธฐํ์ ๋ฐ๋ก ์ ๋ฆฌํด๋ณธ๋ค.