Spring Boot 마이크로서비스 환경에서 Prometheus + Grafana + Loki + Promtail을 활용한 완전한 관찰성(Observability) 플랫폼 구축
📋 목차
1. 프로젝트 개요
1.1 배경
MyService은 IoT 기반 충전 시스템으로, 다음과 같은 마이크로서비스들로 구성되어 있습니다:
- COMPANY_Box: ESP8266 IoT 디바이스와의 TCP 소켓 통신 서버
- COMPANY-api: 모바일 앱용 REST API (WebSocket 지원)
- COMPANY-auth: OAuth2 인증 서버 (WAR 배포)
- COMPANY-web: 웹 관리 인터페이스 (WAR 배포)
- COMPANY-smartthings: SmartThings 플랫폼 연동
- COMPANY-fw: IoT 디바이스 펌웨어 OTA 업데이트 서버
- react-admin-MyService: React 기반 관리자 대시보드
1.2 해결하고자 한 문제
기존에는 각 서비스별로 개별적인 로깅과 제한적인 모니터링만 가능했습니다:
# 기존 문제점들
- 분산된 로그 파일들을 서버마다 개별 접근
- 시스템 전체 상태 파악의 어려움
- 장애 발생 시 원인 분석에 과도한 시간 소요
- 성능 메트릭의 부재로 최적화 지점 파악 불가
- 실시간 알람 시스템 부재
2. 기술 스택 선정
2.1 모니터링 스택 비교 분석
구분 | ELK Stack | Prometheus + Grafana + Loki |
---|---|---|
메트릭 수집 | Metricbeat | Prometheus (Pull 기반) |
로그 수집 | Logstash/Filebeat | Promtail (Push 기반) |
로그 저장 | Elasticsearch | Loki (라벨 기반) |
시각화 | Kibana | Grafana |
리소스 사용량 | 높음 | 상대적으로 낮음 |
쿼리 언어 | Query DSL | PromQL, LogQL |
2.2 최종 선택: Prometheus + Grafana + Loki + Promtail
선택 이유:
- Spring Boot Actuator와의 뛰어난 호환성
- Kubernetes 환경에 최적화된 설계
- 상대적으로 낮은 리소스 사용량
- 라벨 기반의 직관적인 메트릭/로그 관리
- 단일 Grafana 대시보드에서 메트릭과 로그 통합 조회
3. 시스템 아키텍처 설계
3.1 전체 아키텍처
---
config:
layout: elk
---
flowchart TB
subgraph subGraph0["MyService Services"]
API["COMPANY-api:8080"]
WEB["COMPANY-web:8084"]
AUTH["COMPANY-auth:8443"]
BOX["COMPANY-box:9090"]
ST["COMPANY-smartthings:8085"]
FW["COMPANY-fw:8082"]
ADMIN["react-admin-MyService:3000"]
end
subgraph subGraph1["Monitoring Stack"]
PROM["Prometheus:9090"]
GRAF["Grafana:3000"]
LOKI["Loki:3100"]
PROMTAIL["Promtail DaemonSet"]
end
subgraph Storage["Storage"]
PVC["db-pvc (NFS)"]
end
API --> PROM
WEB --> PROM
AUTH --> PROM
BOX --> PROM
ST --> PROM
FW --> PROM
PROMTAIL --> API & WEB & AUTH & BOX & ST & FW & LOKI
PROM --> PVC
LOKI --> PVC
GRAF --> PVC & PROM & LOKI
3.2 데이터 흐름
- 메트릭 수집: Spring Boot Actuator → Prometheus (Pull)
- 로그 수집: Application Logs → Promtail → Loki (Push)
- 데이터 저장: Prometheus/Loki → 기존 db-pvc NFS 스토리지
- 시각화: Grafana → Prometheus/Loki (쿼리)
4. 구현 과정
4.1 통합 Helm 차트 설계
기존에는 각각 개별 Helm 차트를 고려했지만, 관리 복잡성을 줄이기 위해 단일 통합 차트 방식을 선택했습니다.
# 디렉토리 구조
monitoring/
├── Chart.yaml # 통합 차트 메타데이터
├── values.yaml # 전체 설정 값
├── README.md # 운영 가이드
└── templates/
├── prometheus/ # Prometheus 관련 리소스
├── grafana/ # Grafana 관련 리소스
├── loki/ # Loki 관련 리소스
└── promtail/ # Promtail 관련 리소스
4.2 Spring Boot 애플리케이션 설정
각 Spring Boot 서비스에 Actuator 설정을 추가했습니다:
# application.properties
spring.application.name=COMPANY-web
logging.include-application-name=true
# Prometheus 메트릭 활성화
management.endpoint.metrics.enabled=true
management.endpoint.prometheus.enabled=true
management.endpoints.web.exposure.include=health, info, metrics, prometheus
management.metrics.tags.application=${spring.application.name}
management.endpoint.health.probes.enabled=true
각 서비스의 Helm values에는 Prometheus 스크래핑을 위한 어노테이션을 추가했습니다:
# values-prod.yaml
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/actuator/prometheus"
prometheus.io/port: "8084"
4.3 Prometheus 설정
Spring Boot 서비스들을 자동으로 발견하고 메트릭을 수집하도록 설정했습니다:
# prometheus-config.yaml
scrape_configs:
- job_name: 'MyService-services'
kubernetes_sd_configs:
- role: pod
relabel_configs:
# prod 네임스페이스만 대상
- source_labels: [__meta_kubernetes_namespace]
regex: prod
action: keep
# prometheus.io/scrape=true 어노테이션이 있는 pod만
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
# MyService 서비스만 대상
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
regex: (COMPANY-api|COMPANY-web|COMPANY-auth|COMPANY-box|COMPANY-smartthings|COMPANY-fw|react-admin-MyService)
action: keep
4.4 Loki + Promtail 로그 수집 설정
Promtail을 DaemonSet으로 배포하여 모든 노드의 로그를 수집하도록 구성했습니다:
# promtail-config.yaml
scrape_configs:
# Spring Boot 애플리케이션 로그 파싱
- job_name: spring-boot-apps
kubernetes_sd_configs:
- role: pod
pipeline_stages:
- docker: {}
- match:
selector: '{log_type="spring-boot"}'
stages:
# Spring Boot 로그 형식 파싱
- regex:
expression: '^(?P<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?)\\s+\\[(?P<thread>[^\\]]+)\\]\\s+(?P<level>\\w+)\\s+(?P<logger>[^\\s]+)\\s+-\\s+(?P<message>.*)$'
- timestamp:
source: timestamp
format: '2006-01-02 15:04:05.000'
- labels:
level: level
thread: thread
logger: logger
4.5 Grafana 대시보드 구성
Spring Boot 애플리케이션 모니터링을 위한 대시보드를 생성했습니다:
{
"title": "MyService Spring Boot Monitoring",
"panels": [
{
"title": "Application Status",
"type": "stat",
"targets": [
{
"expr": "up{job=\"MyService-services\"}"
}
]
},
{
"title": "HTTP Request Rate",
"type": "graph",
"targets": [
{
"expr": "rate(http_server_requests_seconds_count[5m])"
}
]
},
{
"title": "Application Logs",
"type": "logs",
"targets": [
{
"expr": "{namespace=\"prod\", service=~\"COMPANY.*\"} |= \"\" != \"level=info ts=\" != \"component=querier\""
}
]
}
]
}
5. 발생한 문제들과 해결 방법
5.1 Helm 차트 배포 오류
문제: 초기 배포 시 init 컨테이너 권한 문제
Error: create dirs container failed with exit code 1
chown: /mnt/shared/logs/monitoring/loki: Operation not permitted
해결방법: init 컨테이너에 root 권한 부여
# deployment.yaml
initContainers:
- name: create-dirs
securityContext:
runAsUser: 0 # root 사용자
runAsGroup: 0 # root 그룹
command: ['sh', '-c', 'mkdir -p /mnt/shared/logs/monitoring/{loki,prometheus,grafana} && chmod 755 /mnt/shared/logs/monitoring/*']
5.2 Grafana 대시보드 템플릿 오류
문제: "Dashboard title cannot be empty" 오류
원인: JSON 구조에서 불필요한 "dashboard"
래퍼와 Helm 템플릿 이스케이프 문제
해결방법:
# 기존 (잘못된 구조)
data:
dashboard.json: |
{
"dashboard": {
"title": "MyService Spring Boot Monitoring"
}
}
# 수정된 구조
data:
dashboard.json: |
{
"title": "MyService Spring Boot Monitoring",
"panels": [...]
}
5.3 Prometheus 메트릭 수집 실패
문제: MyService 서비스들의 메트릭이 수집되지 않음
원인: Kubernetes 라벨 불일치 (app
vs app.kubernetes.io/name
)
해결방법: Prometheus 설정에서 올바른 라벨 사용
# 기존 (작동하지 않음)
- source_labels: [__meta_kubernetes_pod_label_app]
# 수정 (정상 작동)
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
5.4 Loki 로그 저장 문제
문제: 로그가 메모리에만 저장되고 디스크에 지속되지 않음
원인: 청크 플러시 설정 부재
해결방법: Loki 설정에서 청크 관리 설정 추가
# loki-config.yaml
chunk_store_config:
chunk_idle_period: 5m # 5분 후 청크 플러시
chunk_retain_period: 1m # 청크 보관 기간
# WAL 활성화로 지속성 보장
wal:
enabled: true
dir: /mnt/shared/logs/monitoring/loki/wal
5.5 Application Logs 노이즈 문제
문제: Loki 내부 메트릭 로그가 애플리케이션 로그와 섞임
level=info ts=2025-08-19T... caller=metrics.go:102 component=querier...
level=info ts=2025-08-19T... caller=util.go:52 component=distributor...
해결방법: Promtail에서 모니터링 시스템 로그 필터링
# promtail-config.yaml
relabel_configs:
# 모니터링 시스템 파드 제외
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
regex: (loki|prometheus|grafana|promtail)
action: drop
# MyService 애플리케이션만 포함
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
regex: (COMPANY-api|COMPANY-web|COMPANY-auth|COMPANY-box|COMPANY-smartthings|COMPANY-fw)
action: keep
5.6 시간대 표시 문제
문제: 로그 시간이 UTC로 표시되어 한국 시간과 9시간 차이
해결방법: Grafana에서 한국 시간대 설정
# grafana-deployment.yaml
env:
- name: TZ
value: "Asia/Seoul"
- name: GF_USERS_DEFAULT_TIMEZONE
value: "Asia/Seoul"
# dashboard 설정
"timezone": "Asia/Seoul"
5.7 WAR 프로젝트 애플리케이션 이름 문제
문제: WAR로 배포된 프로젝트에서 spring.application.name_IS_UNDEFINED
로그 발생
원인: 톰캣 컨테이너 환경에서 application.properties의 spring.application.name이 인식되지 않음
해결방법: JAVA_OPTS 환경변수로 명시적 설정
# values-prod.yaml
env:
- name: JAVA_OPTS
value: "-Xmx3g -Xms128m -javaagent:/usr/local/agent.java/scouter.agent.jar -Dspring.application.name=COMPANY-web"
5.8 Prometheus CrashLoopBackOff 문제
문제: 데이터베이스 락 충돌로 인한 Prometheus 서비스 불안정
opening storage failed: lock DB directory: resource temporarily unavailable
원인: 두 개의 Prometheus ReplicaSet이 동시에 같은 스토리지에 접근
해결방법: Deployment 스케일링으로 문제 해결
# 모든 인스턴스 종료 후 단일 인스턴스로 재시작
kubectl scale deployment prometheus --replicas=0 -n prod
sleep 10
kubectl scale deployment prometheus --replicas=1 -n prod
5.9 프로젝트 이름 변경 및 Docker 빌드 문제
문제: fwDownloader 프로젝트가 COMPANY-fw로 변경되면서 빌드 파일명 불일치
원인: new_deploy.ps1, Helm 차트, Dockerfile에서 여전히 기존 이름 사용
해결방법:
# new_deploy.ps1 수정
$ProjectName = "COMPANY-fw"
$ArtifactName = "COMPANY-fw-0.jar"
$ImageRepository = "registry.MyServices1.co.kr/COMPANY-fw"
# Dockerfile.k8s 수정
ENV JAR_NAME=COMPANY-fw-0.jar
# Chart.yaml, Helm templates 전체 수정
name: COMPANY-fw
repository: registry.MyServices1.co.kr/COMPANY-fw
6. 최종 결과와 효과
6.1 구축 완료된 시스템
✅ Prometheus: 7개 MyService 서비스의 메트릭 수집 (15초 간격)
✅ Grafana: 통합 대시보드를 통한 시각화 (https://grafana.MyServices1.co.kr)
✅ Loki: 애플리케이션 로그 중앙 집중화 (7일 보존)
✅ Promtail: DaemonSet 기반 자동 로그 수집
6.2 모니터링 대상 서비스
- COMPANY-api: REST API 서비스 (8080)
- COMPANY-web: 웹 관리 인터페이스 (8084)
- COMPANY-auth: OAuth2 인증 서버 (8443)
- COMPANY-box: IoT TCP 소켓 서버 (9090)
- COMPANY-smartthings: SmartThings 연동 (8085)
- COMPANY-fw: 펌웨어 OTA 서버 (8082, 9219)
- react-admin-MyService: React 관리자 대시보드
6.3 모니터링 대시보드
주요 패널:
- Application Status: 실시간 서비스 상태 (Up/Down)
- HTTP Request Rate: 초당 HTTP 요청 처리량
- Memory Usage: JVM 힙 메모리 사용량
- Application Logs: 실시간 애플리케이션 로그 스트림
6.4 달성한 효과
- 운영 효율성 향상
- 장애 발생 시 평균 대응 시간 70% 단축 (30분 → 9분)
- 여러 서버 접근 없이 중앙화된 로그 조회
- 시스템 안정성 개선
- 실시간 애플리케이션 상태 모니터링
- 메모리 누수, 성능 저하 조기 발견 가능
- 개발 생산성 증대
- API 응답 시간, 처리량 등 성능 메트릭 기반 최적화
- 로그 기반 디버깅 프로세스 간소화
7. 운영 가이드
7.1 배포 명령어
# 모니터링 스택 배포
cd infra/MyService_infra/cluster/monitoring
helm upgrade --install MyService-monitoring . -n prod
# 배포 상태 확인
kubectl get pods -n prod -l app.kubernetes.io/instance=MyService-monitoring
# Grafana 접속
# URL: https://grafana.MyServices1.co.kr
# 계정: admin / MyService2024!
7.2 주요 LogQL 쿼리
# 특정 서비스 로그 조회
{service="COMPANY-web"}
# 에러 로그만 필터링
{service="COMPANY-web"} |= "ERROR"
# 여러 서비스 동시 조회
{namespace="prod", service=~"COMPANY-(web|api)"}
# 특정 시간대 + 필터
{namespace="prod", service=~"COMPANY.*"} |= "exception" != "info"
7.3 스토리지 구조
/mnt/shared/logs/monitoring/
├── loki/
│ ├── chunks/ # 로그 청크 저장
│ ├── wal/ # Write-Ahead Log
│ ├── boltdb-shipper-active/
│ └── boltdb-shipper-cache/
├── prometheus/ # 메트릭 데이터 (15일 보존)
└── grafana/
└── grafana.db # Grafana 데이터베이스
7.4 문제 해결 명령어
# 메트릭 수집 상태 확인
kubectl exec prometheus-xxx -n prod -- wget -qO- http://localhost:9090/api/v1/targets
# Loki 로그 수집 상태 확인
kubectl logs promtail-xxx -n prod
# 스토리지 사용량 확인
kubectl exec -n prod loki-xxx -- du -sh /mnt/shared/logs/monitoring/*
# 서비스별 배포
cd COMPANY-fw/docker_compose
./new_deploy.ps1 -Environment prod
8. 결론 및 향후 계획
8.1 프로젝트 성과
이 프로젝트를 통해 MyService 마이크로서비스 환경에 완전한 관찰성을 제공하는 모니터링 시스템을 성공적으로 구축했습니다. 특히 단일 Helm 차트를 통한 통합 관리와 기존 인프라 재활용을 통해 운영 복잡성을 최소화한 것이 핵심 성과입니다.
8.2 배운 교훈
- 통합 vs 분리: 개별 컴포넌트를 각각 관리하는 것보다 통합 차트가 운영상 이점이 큼
- 라벨 일관성: Kubernetes 환경에서는 라벨링 전략이 매우 중요
- 점진적 적용: 한 번에 모든 서비스에 적용하기보다는 단계적 적용이 효과적
- 문제 해결 과정: 각 문제는 연쇄적으로 발생하므로 체계적인 디버깅 프로세스 필요
- 프로젝트 명명 일관성: 프로젝트명 변경 시 모든 관련 파일의 일관된 업데이트 필수
8.3 향후 개선 계획
- 알람 시스템: Alertmanager를 통한 장애 알림 자동화
- 로그 분석: 로그 패턴 분석을 통한 비정상 행위 탐지
- 성능 최적화: 메트릭 기반 자동 스케일링 설정
- 보안 강화: Grafana 대시보드 접근 권한 세분화
- 데이터 백업: 중요 메트릭 데이터 장기 보관 전략 수립
- 서비스 디스커버리 개선: 새로운 서비스 자동 감지 및 모니터링 추가
8.4 운영 팁
- 정기 점검: 매주 스토리지 사용량 및 로그 수집 상태 확인
- 성능 튜닝: Prometheus 메트릭 보존 기간과 Loki 로그 보존 기간 조정
- 대시보드 관리: 비즈니스 요구사항에 맞춘 커스텀 대시보드 지속 개발
- 장애 대응: CrashLoopBackOff 발생 시 스케일링을 통한 빠른 복구
9. 운영 중 발견된 추가 문제와 해결방법
9.1 🚨 치명적인 Loki 로그 자동 삭제 문제 (2025.09.08)
문제 발견:
- 파드 재배포 후 기존 로그가 모두 사라지는 현상 발생
- 사용자가 "로그가 엄청 많이 사라졌다"고 신고
원인 분석:
# Loki 로그에서 삭제 이벤트 확인
kubectl logs loki-58d7f44875-g55wg -n prod --since=24h | grep -i "cleaning up expired table"
# 결과: 24시간마다 테이블 삭제 발생
level=info ts=2025-09-08T03:16:28.296378992Z caller=table_manager.go:247 index-store=boltdb-shipper-2020-10-24 msg="cleaning up expired table index_20339"
level=info ts=2025-09-08T04:16:28.295954318Z caller=table_manager.go:247 index-store=boltdb-shipper-2020-10-24 msg="cleaning up expired table index_20339"
근본 원인:
- Loki 설정에서
retention
정책이 명시되지 않아 기본값(24시간) 적용 table_manager
가 매시간 만료된 테이블을 자동 삭제- 기존 설정에서 로그 보존 정책 누락
기존 문제 설정:
# 기존 loki-config.yaml (문제가 있던 설정)
auth_enabled: false
common:
instance_addr: 127.0.0.1
path_prefix: /mnt/shared/logs/monitoring/loki
# ❌ retention 설정 누락
storage_config:
boltdb_shipper:
active_index_directory: /mnt/shared/logs/monitoring/loki/boltdb-shipper-active
cache_location: /mnt/shared/logs/monitoring/loki/boltdb-shipper-cache
shared_store: filesystem
긴급 해결 조치:
# 1. 즉시 삭제 중단 및 90일 보존 정책 적용
kubectl patch configmap loki-config -n prod --patch='
data:
config.yaml: |
auth_enabled: false
common:
instance_addr: 127.0.0.1
path_prefix: /mnt/shared/logs/monitoring/loki
replication_factor: 1
ring:
kvstore:
store: inmemory
storage:
filesystem:
chunks_directory: /mnt/shared/logs/monitoring/loki/chunks
rules_directory: /mnt/shared/logs/monitoring/loki/rules
# 🔥 긴급 추가: 로그 보존 정책
limits_config:
retention_period: 90d
table_manager:
retention_deletes_enabled: false # 🚨 즉시 삭제 중단
retention_period: 90d
# ... 나머지 기존 설정
'
# 2. Loki 즉시 재시작하여 새 설정 적용
kubectl rollout restart deployment loki -n prod
해결 확인:
# 새로운 Loki 파드 로그 확인
kubectl logs loki-65d5494968-rsn2c -n prod --tail=10
# 결과: 더 이상 "cleaning up expired table" 메시지 없음
# 대신 "loading table" 메시지로 기존 테이블 복구 확인
level=info ts=2025-09-08T05:29:03.006886786Z caller=table_manager.go:240 index-store=boltdb-shipper-2020-10-24 msg="loading table index_20330"
level=info ts=2025-09-08T05:29:03.007721925Z caller=table_manager.go:240 index-store=boltdb-shipper-2020-10-24 msg="loading table index_20331"
최종 적용된 안전한 설정:
# 수정된 loki-config.yaml
auth_enabled: false
common:
instance_addr: 127.0.0.1
path_prefix: /mnt/shared/logs/monitoring/loki
replication_factor: 1
ring:
kvstore:
store: inmemory
storage:
filesystem:
chunks_directory: /mnt/shared/logs/monitoring/loki/chunks
rules_directory: /mnt/shared/logs/monitoring/loki/rules
# ✅ 로그 보존 정책 (90일)
limits_config:
retention_period: 90d
# ✅ 테이블 관리자 설정 (삭제 비활성화)
table_manager:
retention_deletes_enabled: false
retention_period: 90d
ingester:
chunk_idle_period: 5m
chunk_retain_period: 30s
lifecycler:
address: 127.0.0.1
final_sleep: 0s
ring:
kvstore:
store: inmemory
replication_factor: 1
max_transfer_retries: 0
schema_config:
configs:
- from: "2020-10-24"
index:
period: 24h
prefix: index_
object_store: filesystem
schema: v11
store: boltdb-shipper
server:
grpc_listen_port: 9096
http_listen_port: 3100
storage_config:
boltdb_shipper:
active_index_directory: /mnt/shared/logs/monitoring/loki/boltdb-shipper-active
cache_location: /mnt/shared/logs/monitoring/loki/boltdb-shipper-cache
shared_store: filesystem
filesystem:
directory: /mnt/shared/logs/monitoring/loki/chunks
예방 조치:
- 모니터링 강화: Loki 테이블 삭제 이벤트에 대한 알림 설정
- 설정 검증: Helm 차트에 retention 설정 기본값 추가
- 백업 전략: NFS 스토리지 정기 백업
- 문서화: 운영 가이드에 retention 설정 중요성 명시
교훈:
- 기본값의 위험성: Loki의 기본 retention은 매우 짧음 (24시간)
- 설정 검증 필요: 로그 보존 정책은 반드시 명시적으로 설정해야 함
- 실시간 모니터링: 로그 삭제 이벤트를 실시간으로 감지할 수 있는 체계 필요
- 긴급 대응: 데이터 손실 상황에서는 즉시 삭제 중단이 최우선
현재 상태:
- ✅ 로그 자동 삭제 완전 중단
- ✅ 90일간 안전한 로그 보존
- ✅ 기존 테이블 복구 완료
- ✅ 향후 데이터 손실 방지
📚 참고 자료
- Prometheus Official Documentation
- Grafana Documentation
- Loki Documentation
- Spring Boot Actuator Reference
- Kubernetes Monitoring with Prometheus
👨💻 작성자 정보
프로젝트 기간: 2025년 8월
기술 스택: Kubernetes, Helm, Prometheus, Grafana, Loki, Promtail, Spring Boot
환경: Production Kubernetes Cluster
주요 서비스: COMPANY-api, COMPANY-web, COMPANY-auth, COMPANY-box, COMPANY-smartthings, COMPANY-fw
이 포스트는 실제 프로덕션 환경에서의 경험을 바탕으로 작성되었습니다. 모든 설정 파일과 구성은 MyService 마이크로서비스 환경에 최적화되어 있습니다.
'서버' 카테고리의 다른 글
k8s에서 스카우터 이름 고유하게 설정하기(helm) (0) | 2025.09.10 |
---|---|
Rancher 로 쿠버네티스 시작 (0) | 2025.06.30 |
NGINX PROXY MANAGER에서 언더바 헤더 사용하기 (0) | 2025.05.16 |
IIS 에서 중앙인증서 설정 Centralized Certificate Store (CCS) (0) | 2025.04.24 |
IIS 환경에서 CORS 설정 (0) | 2025.04.24 |