데이터와 자동화 — 요구조건을 만족시키는 '새로운' 플랫폼만이 해답이었다
핵심: 기존 결과물의 플랫폼 구조로는 CEO가 요구한 서비스를 만들 수 없다. 우리는 아키텍처·데이터·자동화·보안을 전부 새로 설계했고, 그 설계만이 글로벌·다국어·다통화·실시간 흐름(Quote→PO→Invoice→Delivery→Settlement)을 단일 트랜잭션 체인으로 구현한다.
1. 요구조건에서 시작한 ‘새 플랫폼’ 선언
- Quote → PO → Invoice → Delivery → Settlement 전체 흐름이 단일 데이터 체인으로 연결될 것
- 국가/언어/통화/세율/물류 규칙을 데이터 레이어에서 통합 관리할 것
- 데이터 변화가 이벤트(AMQP)를 통해 실시간 전파되고 캐시와 UI가 자동 갱신될 것
- 사람이 문서/툴을 조작하지 않아도 AI 워크플로우가 명세→스키마→API→배포를 자동 생성할 것
이 요구는 곧 플랫폼의 재정의였다. “코드가 데이터를 설명”하던 과거에서, “데이터가 코드를 지배하는” 구조로의 전환. 이 관점에서 전 계층을 다시 설계했다.
2. 새로운 기술 스택(Infra/Runtime/Automation/Security)
Infra & Network
- Tencent Cloud: ap-hongkong(Primary), ap-seoul(DR)
- DNSPod Geo Routing + Health Check
- COS + CDN: 정적 자산 전세계 엣지 배포
- TKE/CVM: API 모듈 컨테이너 오케스트레이션
Data & Messaging
- TencentDB for MySQL 8.0 + DTS(GTID, Async)
- Redis Cluster (Session/Cache, TTL 정책)
- LavinMQ(RabbitMQ 호환, Topic Exchange)
Automation
- Drone CI/CD (self-hosted)
- AI Workflow Engine (명령→DBML/Swagger→배포 자동화)
- Cloud Functions (DTS/캐시/장애 대응)
Security & Observability
- mTLS 내부 통신, JWT 서비스 인증
- IAM/KMS 최소권한·비밀관리, Cloud Audit 추적성
- Cloud Monitor + Grafana 지표/알림
프런트는 Nuxt 3 + Vue 3, 서버는 내부 API 게이트웨이 뒤에서만 동작한다(백엔드는 비공개). 글로벌 접근은 DNSPod가 지역 엣지로 라우팅, 정적은 CDN, 동적은 Gateway로 분기된다.
3. 데이터 모델 — 60 → 250개, ‘의미 있는’ 정규화
CEO 요구를 충족하려면 테이블 수가 늘어나는 게 목적이 아니라, 관계와 규칙이 데이터에 내재되어야 한다. 새 모델은 6계층으로 조직했다.
- ① Master: 통화/세율/국가/언어
- ② Partner: 고객/공급사/담당자
- ③ Transaction: Quote/PO/Invoice/Settlement
- ④ Logistics: 선적/통관/트래킹
- ⑤ Integration: 환율/외부 인증/웹훅
- ⑥ Audit: 변경 이력/접근 로그
핵심 스키마(SQL, 발췌)
CREATE TABLE currency_master (
code CHAR(3) PRIMARY KEY,
symbol VARCHAR(8),
country_code CHAR(2),
exchange_rate DECIMAL(18,6) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE customer_company (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
country_code CHAR(2) NOT NULL,
currency_code CHAR(3) NOT NULL,
language_code VARCHAR(5) NOT NULL,
CONSTRAINT fk_customer_currency FOREIGN KEY (currency_code) REFERENCES currency_master(code)
);
CREATE TABLE quote (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
customer_id BIGINT NOT NULL,
supplier_id BIGINT NOT NULL,
quote_date DATETIME NOT NULL,
total_amount DECIMAL(18,2) NOT NULL,
status ENUM('OPEN','APPROVED','EXPIRED','REJECTED') NOT NULL DEFAULT 'OPEN',
CONSTRAINT fk_quote_customer FOREIGN KEY (customer_id) REFERENCES customer_company(id)
);
CREATE TABLE order_po (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
quote_id BIGINT NOT NULL,
po_number VARCHAR(50) UNIQUE,
payment_terms VARCHAR(255),
expected_delivery DATETIME,
CONSTRAINT fk_po_quote FOREIGN KEY (quote_id) REFERENCES quote(id) ON DELETE CASCADE
);
CREATE TABLE invoice (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
po_id BIGINT NOT NULL,
invoice_number VARCHAR(50) UNIQUE,
vat_rate DECIMAL(5,2) NOT NULL,
payment_status ENUM('UNPAID','PARTIAL','PAID') NOT NULL DEFAULT 'UNPAID',
issued_date DATETIME NOT NULL,
CONSTRAINT fk_invoice_po FOREIGN KEY (po_id) REFERENCES order_po(id)
);
CREATE TABLE logistics_tracking (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
po_id BIGINT NOT NULL,
vessel_name VARCHAR(100),
etd DATETIME,
eta DATETIME,
tracking_no VARCHAR(100),
tracking_url VARCHAR(255),
CONSTRAINT fk_tracking_po FOREIGN KEY (po_id) REFERENCES order_po(id)
);
CREATE TABLE settlement (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
invoice_id BIGINT NOT NULL,
settlement_currency CHAR(3) NOT NULL,
exchange_rate DECIMAL(18,6) NOT NULL,
payment_confirmed DATETIME,
CONSTRAINT fk_settlement_invoice FOREIGN KEY (invoice_id) REFERENCES invoice(id)
);
이 구조는 거래의 전 구간을 한 체인으로 연결한다. 환율/세율/언어는 Master 계층에서 일원화되고, 변경 즉시 다운스트림에 전파된다.
4. API 명세 자동화 — 스키마로부터 생성
AI Workflow Engine이 DB 스키마를 읽고 Swagger(OpenAPI)를 생성한다. 인적 문서 작업 없이, 데이터가 곧 API가 된다.
{
"openapi": "3.0.3",
"info": { "title": "REINDEERS API", "version": "2025-05" },
"paths": {
"/po": {
"get": { "summary": "List POs", "responses": { "200": { "description": "OK" } } },
"post": {
"summary": "Create PO",
"requestBody": { "$ref": "#/components/requestBodies/CreatePO" },
"responses": { "201": { "description": "Created" } }
}
}
},
"components": {
"schemas": {
"PO": {
"type": "object",
"properties": {
"id": { "type": "integer", "format": "int64" },
"po_number": { "type": "string" },
"expected_delivery": { "type": "string", "format": "date-time" }
}
}
},
"requestBodies": {
"CreatePO": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PO" }}}}
}
}
}
5. 자동 배포 — Drone CI/CD
배포는 Git push로 트리거된다. 동일 YAML로 Dev/Prod 분기, COS 업로드·CDN 퍼지·헬스체크까지 자동화.
kind: pipeline
type: docker
name: deploy
trigger:
branch: [ main, develop ]
steps:
- name: build
image: node:20
commands:
- npm ci
- npm run lint
- npm run test
- npm run build
- name: upload
image: tencentcloud/tencentcloud-cli
environment:
COS_BUCKET: reindeers-fe
COS_REGION: ap-hongkong
commands:
- tccli cos PutObject --bucket $COS_BUCKET --region $COS_REGION \
--file ./.output/public --key / --acl public-read
- name: purge-cdn
image: alpine/curl
commands:
- curl -X POST https://cdn.tencentcloudapi.com/purge?path=https://static.reindeers.com/assets/*
- name: notify
image: plugins/slack
settings:
webhook: ${SLACK_WEBHOOK}
channel: "#deploy"
template: "✅ Deploy {{build.number}} for {{build.branch}}"
6. 실시간 데이터 동기화 — DTS + Redis + MQ
핵심은 사람 개입 없는 일관성이다. 데이터 변경 → 이벤트 발행 → 캐시 무효화 → DR 복제 감시까지 자동.
# DTS (개념 구성)
Source: MySQL (ap-hongkong, Primary)
Target: MySQL (ap-seoul, DR)
Mode: Asynchronous (GTID)
LatencyThreshold: 500ms
Recovery: AutoResume
# Redis 캐시 정책
cache.hit_target = >= 80%
cache.ttl.default = 300s
session.ttl = 3600s
# MQ (Topic Exchange)
exchange: reindeers.global
bindings:
- routing_key: order.created -> queue: cache_invalidator
- routing_key: invoice.updated -> queue: finance_worker
MQ-Redis 연동 코드(Python)
import json, pika, redis
r = redis.StrictRedis(host="redis.internal", port=6379)
mq = pika.BlockingConnection(pika.ConnectionParameters("mq.internal"))
ch = mq.channel()
ch.queue_declare(queue="cache_invalidator", durable=True)
def on_msg(ch, method, props, body):
evt = json.loads(body)
r.delete(f"cache:{evt['table']}:{evt['id']}")
ch.basic_ack(method.delivery_tag)
ch.basic_consume(queue="cache_invalidator", on_message_callback=on_msg)
ch.start_consuming()
7. 보안 — ‘기본이 강한’ 운영
- mTLS 서비스 간 통신, Private Endpoint만 허용
- JWT + 롤 기반 접근제어(RBAC)
- IAM/KMS 최소권한 정책, 비밀은 런타임에서만 복호화
- Cloud Audit 전 활동 추적, 변경 불가능(append-only) 저장
{
"Version": "2.0",
"Statement": [{
"Effect": "Allow",
"Action": [ "cos:PutObject", "cdn:PurgePathCache" ],
"Resource": "qcs::cos:ap-hongkong:uid/1234567890:reindeers-*/*"
}]
}
8. 관측·복구 — 모니터링이 곧 운영
Cloud Monitor→Function→Alert→Grafana까지 폐루프로 구성. 임계치 초과 시 자동 복구·알림.
# Function (파이썬, DTS 지연 자동 복구)
def handler(evt, ctx):
import requests
delay = evt["metrics"]["dts_delay_ms"]
if delay > 500:
requests.post("https://api.tencentcloud.com/restart_dts",
json={"task_id":"dts-hk-kr"})
requests.post("${SLACK_WEBHOOK}",
json={"text": f"DTS delay {delay}ms - task restarted"})
대시보드는 DTS 지연, Redis hit ratio, MQ backlog, API P95, 배포 성공률을 기본 지표로 삼는다.
9. 결론 — 왜 ‘새’ 플랫폼만 정답인가
CEO의 요구는 “데이터로 비즈니스를 설명하고, 변화가 시스템을 자동으로 움직이는 구조”였다. 이를 만족하려면:
- 데이터가 전 구간에서 체인으로 연결되어야 한다.
- 국가/언어/통화/세율/물류 규칙이 데이터 레이어에서 일원화돼야 한다.
- 변화가 이벤트로 전파되어 캐시·UI·DR이 자동 갱신돼야 한다.
- 명세→스키마→API→배포가 AI 워크플로우로 자가 생성·자가 검증돼야 한다.
기존 결과물의 플랫폼 구조로는 이 요구를 달성할 수 없다. 그래서 우리는 인프라·데이터·자동화·보안을 처음부터 다시 만들었다. 지금의 REINDEERS는 “사람이 문서를 쓰는 조직”이 아니라, “데이터가 시스템을 스스로 구성하는 플랫폼”이다.
Comments
Post a Comment