토스/쿠버네티스

5 개의 포스트

toss6분 읽기큐레이션 요약

Spark Connect on Kubernetes #1: 견고한 Spark Connect 만들기

토스증권은 여러 사용자가 안정적으로 사용할 수 있는 Production급 Spark Connect를 Kubernetes에서 운영하고 있습니다. Spark Connect는 Driver를 애플리케이션마다 실행하는 대신 장기 실행 서버로 분리해 가벼운 클라이언트와 빠른 세션 생성을 제공하지만, 여러 세션이 하나의 SparkContext를 공유하면서 장애 전파와 리소스 경합 문제가 발생합니다. 이를 해결하기 위해 글로벌 장애 카운터를 사실상 비활성화하고, 결과 크기를 제한하며, 여러 Replica로 Driver와 SparkContext를 분리하는 전략을 사용합니다. ## Classic Spark의 구조와 Spark Connect의 등장 - Spark는 작업을 계획·지휘하는 **Driver**와 실제 연산을 수행하는 **Executor**로 구성됩니다. - Classic Spark의 배포 방식은 다음과 같습니다. - **Client mode**: 클라이언트 프로세스가 Driver 역할을 수행합니다. - **Cluster mode**: 작업 제출 시 클러스터에 Driver가 생성되고 작업 종료 후 사라집니다. - 두 방식 모두 애플리케이션마다 Driver가 하나씩 생성되고, 애플리케이션의 수명과 함께 종료됩니다. - Spark Connect는 Spark 3.4부터 도입됐으며, 4.0에서는 기존 Dataset/DataFrame API와 거의 동등한 수준에 도달했습니다. - 글의 구현과 설정은 Spark 4.1을 기준으로 합니다. ## Spark Connect의 동작 방식 - Spark Connect에서는 Driver를 애플리케이션별 프로세스가 아니라 **미리 실행해 둔 서버**로 운영합니다. - 클라이언트는 Spark 라이브러리와 JVM을 직접 포함하지 않는 Thin Client입니다. - 클라이언트의 DataFrame·SQL 연산은 다음 과정으로 처리됩니다. - 연산을 Unresolved Logical Plan으로 변환 - Protocol Buffer로 인코딩 - gRPC를 통해 서버로 전송 - 서버가 분석, 최적화, 스케줄링, 실행 수행 - 결과를 Arrow 기반으로 클라이언트에 스트리밍 - 구조적으로는 JDBC 클라이언트가 데이터베이스 서버에 질의하는 모델과 유사합니다. ## Spark Connect의 장점 - 클라이언트에 무거운 Spark 의존성이나 JVM이 없어도 됩니다. - Python, SQL, 노트북, BI 도구 등 다양한 클라이언트가 같은 서버에 접속할 수 있습니다. - Driver가 이미 실행 중이므로 매번 프로세스를 생성하고 리소스를 협상할 필요가 없습니다. - 클라이언트가 종료되거나 네트워크가 끊겨도 서버에서 실행 중인 작업은 보호할 수 있습니다. - 반면 하나의 장기 실행 서버에 여러 사용자가 접속하면서, Spark의 기존 “애플리케이션 하나에 워크로드 하나”라는 전제가 깨집니다. ## 공유 Driver가 만드는 단일 장애점 - 여러 세션이 하나의 SparkContext와 Driver JVM을 공유합니다. - Driver가 장애를 일으키면 해당 서버의 모든 세션, 실행 중인 Job, 캐시가 함께 사라집니다. - `spark.executor.maxNumFailures`는 Executor 실패를 애플리케이션 전체 단위로 누적합니다. - 기본 임계값은 `max(3, 2 × executor 수)`입니다. - 임계값을 초과하면 `stopApplication()`이 호출되고, 결과적으로 `sys.exit(11)`로 서버 전체가 종료됩니다. - 이 카운터는 다음 이유로 멀티세션 환경에서 위험합니다. - 개별 쿼리의 Task 실패가 아니라 Executor 실패를 전역적으로 집계합니다. - 시간이 지나도 실패 기록이 계속 누적됩니다. - 서로 다른 사용자의 실패가 합산됩니다. - 문제가 없는 세션도 장애를 함께 겪게 됩니다. ## 세션 격리와 리소스 경합의 한계 - `newSession()`은 SQL 네임스페이스 등 세션 상태만 분리합니다. - CPU, 메모리, Executor, Task 슬롯은 모든 세션이 공유합니다. - 한 사용자가 대규모 Job을 제출하면 다른 사용자의 쿼리 응답도 느려질 수 있습니다. - 기본 FIFO 스케줄링에서는 먼저 제출된 작업이 우선하며, 선점이 없어 이미 실행 중인 Task를 중단할 수 없습니다. - Fair Scheduler를 사용해도 Task 슬롯을 배분하는 순서만 조정할 뿐, 사용자별 CPU·메모리 격리는 제공하지 않습니다. - Spark Connect에서는 `spark.scheduler.pool`이 기본적으로 제대로 전파되지 않아 모든 쿼리가 Default Pool에 들어갑니다. - Classic Spark에서는 `setLocalProperty()`가 Driver 스레드에 직접 적용됩니다. - Spark Connect에서는 클라이언트와 Driver가 분리되어 서버의 요청 처리 스레드에 값을 별도로 설정해야 합니다. - 토스증권은 서버 스레드에 사용자별 Pool을 직접 설정하는 방식으로 이 문제를 보완했습니다. - 사용자별 Pool을 적용하려면 먼저 요청의 사용자를 식별해야 하며, 인증·인가와 연결됩니다. - 궁극적인 CPU·메모리 격리는 Spark 스케줄러가 아니라 Spark 외부의 리소스 관리 계층에서 해결해야 합니다. ## 고정된 서버 스케일 문제 - Spark Connect 서버는 이미지, Driver·Executor 리소스, Spark 설정이 고정된 상태로 실행됩니다. - Dynamic Resource Allocation으로 Executor 수는 조절할 수 있지만, 서버 자체의 기본 스펙은 실행 중 바뀌지 않습니다. - 서버를 필요에 따라 생성·교체하거나 팀 단위로 격리하는 문제는 후속 글에서 다룹니다. ## 글로벌 장애 카운터 비활성화 - 서버 전체를 종료시키는 Executor 실패 경로를 차단하기 위해 다음과 같이 설정합니다. - `spark.executor.maxNumFailures`: 사실상 무한대로 설정해 글로벌 종료 조건을 비활성화 - `spark.executor.failuresValidityInterval`: 오래된 실패 기록을 주기적으로 제거 - `spark.task.maxFailures`: 동일 Task의 반복 실패를 제한 - `spark.stage.maxConsecutiveAttempts`: Shuffle Fetch 실패로 Stage가 반복 실행되는 상황을 제한 - `task.maxFailures`는 OOM이나 예외처럼 동일 Task가 반복 실패하는 경우를 담당합니다. - `stage.maxConsecutiveAttempts`는 Shuffle Fetch 실패로 Stage 전체가 반복되는 경우를 담당합니다. - 이 방식으로 문제가 있는 쿼리만 실패시키고 서버와 다른 사용자의 세션은 유지할 수 있습니다. - 다만 실패 허용 횟수를 지나치게 낮추면 일시적인 장애에도 정상 쿼리가 실패할 수 있으므로 워크로드에 맞춰 여유를 둬야 합니다. ## Driver 메모리 보호와 결과 크기 제한 - Spark Connect에서는 쿼리 결과가 Driver를 거쳐 클라이언트로 스트리밍됩니다. - 사용자가 대규모 테이블을 `collect`하면 Driver 메모리가 고갈될 수 있습니다. - `spark.driver.maxResultSize`는 한 액션에서 반환되는 Task 결과의 누적 크기를 제한합니다. - 제한을 초과하면 Driver가 결과를 모두 가져오기 전에 Job을 중단하므로, 대규모 결과가 Driver 메모리에 유입되는 것을 막을 수 있습니다. - 기본값인 1GB는 애플리케이션 하나만 실행하는 환경의 값이므로, 여러 세션이 동시에 결과를 가져가는 멀티세션 서버에서는 더 보수적으로 설정해야 합니다. - 이 설정만으로 Driver OOM이나 노드 장애까지 막을 수는 없습니다. ## 여러 Replica를 통한 장애 영향 축소 - Driver 자체의 OOM이나 노드 소실처럼 설정으로 막을 수 없는 장애에 대비해 Spark Connect 서버를 여러 Replica로 구성합니다. - 각 Replica는 독립적인 다음 요소를 갖습니다. - SparkContext - Driver - Executor - 한 Replica가 장애로 종료되어도 장애 범위가 해당 Replica에 한정되고, 다른 Replica가 새로운 세션 요청을 처리할 수 있습니다. - 단일 서버의 장애가 Spark Connect 전체로 확산되는 구조를 여러 독립 실행 단위로 나누는 것이 핵심입니다. ## 실용적인 운영 방향 - 멀티세션 Spark Connect에서는 전역 장애 카운터를 그대로 두지 말고, Task·Stage·Job 단위의 실패 제한으로 문제 쿼리를 격리하는 것이 안전합니다. - `spark.driver.maxResultSize`를 동시 세션 수와 쿼리 특성에 맞게 보수적으로 설정해야 합니다. - 스케줄러 Pool만으로는 CPU·메모리 격리가 불가능하므로, 강한 격리가 필요하면 Replica나 Kubernetes 리소스 정책을 활용해야 합니다. - 단일 Driver를 그대로 공유하기보다 여러 Replica를 운영해 장애의 영향 범위를 줄이는 것이 Production 환경에 적합합니다.

원문 읽기(새 탭에서 열림)
toss원문

소프트웨어 3.0 시대를 맞이하며 (새 탭에서 열림)

소프트웨어 개발은 명시적 코딩(1.0)과 데이터 기반 학습(2.0)을 거쳐, 자연어 프롬프트가 프로그램이 되는 '소프트웨어 3.0' 시대로 진입하고 있습니다. 하지만 강력한 LLM 모델이라도 실질적인 업무를 수행하기 위해서는 모델의 능력을 제어하고 연결하는 '하네스(Harness)'라는 도구적 환경이 필수적이며, 이를 설계하는 데 있어 기존 소프트웨어 1.0의 계층형 아키텍처 원칙은 여전히 유효한 가이드가 됩니다. 결국 미래의 개발은 전통적인 설계 원칙을 유지하면서도, 에이전트가 인간과 소통하며 의사결정을 내리는 'Human-in-the-Loop(HITL)' 모델을 결합하는 방향으로 진화할 것입니다. **소프트웨어 3.0과 하네스의 필요성** - 안드레 카파시는 소프트웨어 3.0을 자연어로 된 프롬프트가 코드를 대신하는 시대로 정의하며, 이것이 이전 세대의 패러다임을 흡수할 것이라고 예측했습니다. - 하지만 LLM 단독으로는 코드베이스를 읽거나 데이터베이스에 접근하는 등의 실질적인 작업을 수행할 수 없다는 한계가 있습니다. - 이를 해결하기 위해 등장한 것이 '하네스(Harness)' 개념으로, 앤스로픽의 'Claude Code'처럼 모델이 도구(Skills)를 사용하고 외부와 통신하며 에이전트로 동작하게 만드는 실행 환경을 의미합니다. **계층형 아키텍처로 매핑한 에이전트 구조** - **슬래시 커맨드(Slash Command) = 컨트롤러(Controller):** `/review`, `/refactor`와 같은 명령어는 사용자 요청을 받아 적절한 워크플로우를 실행하는 서비스의 진입점 역할을 합니다. - **서브 에이전트(Sub-agent) = 서비스 계층(Service Layer):** 여러 기술(Skills)을 조합해 특정 비즈니스 로직을 완수하며, 독립적인 컨텍스트를 유지하는 단위입니다. - **기술(Skills) = 도메인 컴포넌트:** 단일 책임 원칙(SRP)에 따라 코드 리뷰, 테스트 생성 등 명확한 한 가지 기능만 수행하는 가장 작은 단위의 기능 모듈입니다. - **MCP(Model Context Protocol) = 인프라/어댑터:** 외부 API나 DB와의 연결을 추상화하여 내부 로직이 외부 시스템의 구현 상세를 몰라도 동작하게 돕습니다. - **CLAUDE.md = 프로젝트 헌장:** 기술 스택, 코딩 컨벤션 등 프로젝트의 변하지 않는 근간 원칙을 정의하며 시스템의 안정성을 보장합니다. **에이전트 설계에서 경계해야 할 안티패턴** - **God Sub-agent:** 하나의 서브 에이전트가 너무 많은 역할과 권한을 가지게 되면 관리 효율이 떨어지므로 적절한 분리가 필요합니다. - **기능 편애(Feature Envy):** 특정 기술이 자신의 역할 범위를 벗어나 다른 기술의 데이터나 프롬프트에 과도하게 의존하는 경우입니다. - **프롬프트 중복:** 동일한 프롬프트 내용이 여러 기술에 중복되어 포함될 경우 유지보수가 어려워지므로 공통화가 필요합니다. **에이전트만의 핵심 차별점: 질문하는 능력(HITL)** - 전통적인 소프트웨어는 예외 상황에서 미리 정의된 에러를 던지지만, 3.0 시대의 에이전트는 `UserAskQuestion` 기술을 통해 모호한 상황에서 사용자에게 직접 질문을 던질 수 있습니다. - 에이전트는 삭제나 배포처럼 되돌리기 어려운 작업, 혹은 여러 대안 중 선택이 필요한 고위험 상황에서 인간의 판단을 구하는 'Human-in-the-Loop' 구조를 가집니다. - 반면, 관습적으로 처리 가능한 일이나 안전한 반복 작업은 질문 없이 자율적으로 수행함으로써 효율성과 안정성 사이의 균형을 맞춥니다. 소프트웨어 3.0 시대에 적응하기 위해서는 모든 로직을 명시적으로 작성하려는 강박에서 벗어나야 합니다. 대신 계층 분리, 추상화, 단일 책임 원칙과 같은 전통적인 소프트웨어 공학의 정수를 에이전트 설계에 투영하여, LLM을 단순한 자동완성 도구가 아닌 신뢰할 수 있는 협력자로 구축하는 능력이 핵심 경쟁력이 될 것입니다.

toss원문

경계 보안부터 제로트러스트 보안까지, 고도화 여정 (새 탭에서 열림)

토스페이먼츠는 기존의 단일 방어선 중심의 열악한 보안 환경을 극복하고, 지난 4년간 IDC와 AWS를 아우르는 하이브리드 환경에서 체계적인 다층 방어(Defense in Depth) 체계를 구축했습니다. 암호화 트래픽 가시성 확보부터 컨테이너 런타임 보안까지 단계별 방어 전략을 수립하여, 외부 공격 차단은 물론 내부 침투와 이상 행위까지 실시간으로 탐지하고 대응할 수 있는 고도화된 보안 기틀을 마련했습니다. ### 경계보안 고도화 및 하이브리드 체계 구축 * **암호화 트래픽 가시성 확보**: HTTPS로 암호화된 트래픽 속에 숨겨진 악성 페이로드를 탐지하기 위해 SSL/TLS 복호화 기능을 전면 도입하여 보안 사각지대를 해소했습니다. * **하이브리드 보안 아키텍처**: IDC에는 DDoS 방어, SSL 복호화, IPS/WAF 이중 보안을 배치하고, AWS에는 AWS WAF와 GuardDuty를 활용한 AI 기반 위협 탐지 체계를 구축했습니다. * **가맹점 협력적 대응**: 가맹점을 통한 악성 트래픽 유입 시 단순히 차단하는 데 그치지 않고, 공격 유형과 조치 가이드를 포함한 상세 안내를 통해 가맹점과 함께 보안 수준을 높이는 생태계를 조성했습니다. ### 서버단 내부망 보안 및 측면 이동 방어 * **Wazuh 통합 모니터링(IDC)**: 오픈소스 보안 플랫폼인 Wazuh를 도입하여 서버 간 측면 이동(Lateral Movement) 공격을 감시하고, 여러 OS의 시스템 및 인증 로그를 중앙에서 통합 관리합니다. * **지능형 위협 탐지(AWS)**: GuardDuty의 Malware Protection을 활용해 EC2 인스턴스의 악성코드를 스캔하고, 일반 계정의 루트 권한 획득(Privilege Escalation)과 같은 이상 징후를 실시간으로 포착합니다. * **실시간 알림 및 화이트리스트**: 서버 내 이상 행위에 대한 실시간 알림 체계를 구축하고, 화이트리스트 기반의 예외 관리를 통해 탐지 효율성을 극대화했습니다. ### 컨테이너 런타임 보안과 최후의 방어선 * **Falco 기반 실시간 감시**: 빠르게 변하는 컨테이너 환경을 보호하기 위해 CNCF 오픈소스 도구인 Falco를 도입, 시스템 호출(Syscall)을 실시간으로 분석하여 비정상적인 행동을 탐지합니다. * **런타임 위협 식별**: 컨테이너 내부에서의 민감 파일(`/etc/shadow` 등) 접근, 신규 바이너리 실행, 컨테이너 탈출 시도 등을 즉각적으로 식별합니다. * **이벤트 전달 체계**: Falco Sidekick을 통합하여 탐지된 보안 이벤트를 실시간으로 관련 시스템에 전달함으로써, 경계 및 서버 보안을 우회한 공격에 대해서도 즉각적인 대응이 가능하도록 설계했습니다. 단순히 외부 침입을 막는 것을 넘어, 내부망의 모든 움직임을 검증하고 가맹점과 보안 가치를 공유하는 다각적인 접근이 현대적인 금융 보안의 핵심입니다. 기술적 솔루션 도입과 더불어 보안 사고 발생 시 파트너사가 자생력을 가질 수 있도록 돕는 협력 모델을 구축하는 것이 지속 가능한 보안 환경을 만드는 실무적인 정답이 될 것입니다.

toss원문

레거시 인프라 작살내고 하이브리드 클라우드 만든 썰 (새 탭에서 열림)

토스페이먼츠는 20년 된 레거시 인프라의 비효율성을 극복하기 위해 오픈소스 기반의 OpenStack 프라이빗 클라우드를 직접 구축하고, 이를 퍼블릭 클라우드와 결합한 'Active-Active 하이브리드 클라우드' 환경을 구현했습니다. 단 2명의 엔지니어가 운영 경험 없이 시작했음에도 불구하고 자동화와 고가용성 전략을 통해 인프라 제어권을 100% 확보했으며, 결과적으로 어떤 환경에서도 즉시 배포 가능한 유연한 기술 기반을 마련했습니다. ### 1,997개의 라우팅이 보여주는 레거시 인프라의 한계 * 과거 인수한 인프라는 네트워크 장비가 아닌 개별 서버가 직접 라우팅 정보를 관리하는 비정상적인 구조로, 서버당 약 2,000개의 라우팅 경로가 설정되어 있었습니다. * 새로운 경로 추가 시 모든 서버를 일일이 수정해야 하는 관리 포인트의 과부하가 발생했으며, 이는 서비스 확장의 심각한 병목 현상이 되었습니다. * 초기에는 퍼블릭 클라우드 도입으로 대응했으나 비용 증가, 환율 변동, 하이브리드 DR 구성의 어려움 및 가시성 부족이라는 새로운 문제에 직면했습니다. ### OpenStack 기반 프라이빗 클라우드 내재화 * 상용 솔루션 대신 오픈소스인 OpenStack을 선택하여 기술 내재화와 유연한 인스턴스 타입(VM, Container, K8S) 대응력을 확보했습니다. * 부족한 운영 경험을 극복하기 위해 3가지 버전의 OpenStack을 수십 번 설치하고 장애 시나리오를 반복 재현하며 아키텍처 이해도를 높였습니다. * 로드밸런서인 옥타비아(Octavia)의 소스 코드를 직접 수정하여 비즈니스 요구에 맞는 로그 포맷을 생성하는 등 오픈소스의 이점을 극대화했습니다. ### 자동화와 모니터링을 통한 운영 효율 극대화 * Ansible과 Terraform 코드를 활용해 모든 자원의 라이프사이클을 자동화했으며, 골든 이미지를 통해 신규 인스턴스 생성 시간을 10초 이내로 단축했습니다. * Zabbix, Prometheus, Mimir, Grafana 등 다양한 오픈소스 툴을 조합하여 모든 메트릭을 수집하고, 실시간 알람 체계를 구축해 장애 감지 능력을 높였습니다. * 운영 인력의 한계를 극복하기 위해 CMDB와 연동된 봇(Bot)을 구현하여 인프라 현황을 실시간으로 조회하고 관리할 수 있도록 했습니다. ### 고가용성을 위한 다중 클러스터 및 Cluster API 전략 * 장애 발생 시 서비스 가용성을 즉시 확보하기 위해 서로 독립된 3개의 OpenStack 클러스터를 구축하고 평상시 Active-Active로 운영합니다. * 특정 클러스터 장애 시 트래픽을 즉시 차단하는 방식으로 복구 시간을 최소화했으며, 클러스터 간 의존성을 완전히 제거했습니다. * K8S 관리를 위해 Cluster API(CAPI)를 도입하여 쿠버네티스 클러스터 자체를 쿠버네티스 리소스로 관리함으로써 퍼블릭 클라우드 수준의 관리 편의성을 프라이빗 환경에서도 구현했습니다. 전통적인 금융 인프라의 보수성을 탈피하고 오픈소스 기술을 깊이 있게 내재화한다면, 퍼블릭 클라우드의 편리함과 온프레미스의 통제권을 동시에 거머쥘 수 있습니다. 인력 부족이나 기술적 난도는 자동화와 표준화된 도구(CAPI, Terraform 등)를 통해 충분히 극복 가능하므로, 비용 최적화와 기술적 가시성이 필요한 조직이라면 하이브리드 클라우드 전략을 적극 권장합니다.

toss원문

수천 개의 API/BATCH 서버를 하나의 설정 체계로 관리하기 (새 탭에서 열림)

토스페이먼츠는 수천 개의 API 서버와 배치 설정을 관리하기 위해 설정을 단순한 텍스트가 아닌 '진화하는 코드'로 정의하여 운영합니다. 복사-붙여넣기식의 중복 설정을 제거하기 위해 오버레이 아키텍처와 템플릿 패턴을 도입했으며, 이를 통해 오타나 설정 오류로 인한 대규모 정산 장애 리스크를 원천 차단합니다. 결과적으로 인프라 설정을 테스트 가능한 영역으로 끌어올려 대규모 하이브리드 클라우드 환경에서도 높은 안정성과 유연성을 확보했습니다. ### 실시간 API 서버: 오버레이와 템플릿의 결합 * **오버레이 아키텍처:** 설정을 `global`, `cluster`, `phase`, `application` 순서의 계층형 구조로 설계하여 하위 계층이 상위 계층의 기본값을 덮어쓰도록 구성했습니다. 이를 통해 공통 설정은 한 번만 정의하고 각 환경에 필요한 차이점만 관리할 수 있습니다. * **템플릿 패턴 도입:** YAML의 단순 오버레이만으로는 해결하기 어려운 긴 문자열(예: JVM 옵션) 내의 특정 값만 수정하기 위해 `{{MAX_HEAP}}`과 같은 변수 치환 방식을 사용합니다. * **동적 설정 주입:** 설정 파일 내부에 파이썬 스크립트를 삽입하여 랜덤 포트 생성이나 외부 API 호출을 통한 동적 값 할당이 가능하며, 클러스터 이름에 따른 조건부 로직을 적용해 복잡한 환경 변수 요구사항을 해결합니다. ### 배치 서버: DSL과 GitOps를 통한 단순화 * **Jenkins 기반의 단순화:** 대규모 정산 데이터를 다루는 배치 환경일수록 단순함이 강력하다는 원칙 아래, Jenkins를 활용하면서도 수동 조작의 단점을 보완하는 방향을 택했습니다. * **Groovy DSL 활용:** Jenkins의 웹 UI를 통한 수동 설정을 배제하고, Groovy 기반의 자체 DSL(Domain Specific Language)을 구축하여 수천 개의 배치 Job을 코드 형태로 관리합니다. * **GitOps 체계:** 모든 배치 설정을 코드 저장소에서 관리하고 CI/CD 파이프라인과 통합함으로써, 개발자가 직접 Jenkins에 접속하지 않고도 표준화된 환경에서 배치 작업을 배포할 수 있도록 개선했습니다. ### 인프라의 코드화와 검증 자동화 * **테스트 가능한 설정:** 설정값에 대한 오타나 논리적 오류를 방지하기 위해 설정 코드에 대한 유닛 테스트를 수행합니다. 이를 통해 수천 개의 설정 중 단 하나의 오타가 치명적인 금융 장애로 이어지는 것을 사전에 방지합니다. * **유연한 확장성:** 고정된 설정 체계에 안주하지 않고, 인프라의 변화와 개발자의 요구사항에 맞춰 설정 인프라 자체가 계속해서 진화할 수 있는 구조를 지향합니다. 단순히 설정 파일을 잘 작성하는 것에 그치지 않고, 인프라 설정을 애플리케이션 코드와 동일한 수준의 설계와 테스트를 거쳐 관리하는 것이 대규모 시스템의 안정성을 보장하는 핵심입니다. 초기에 다소 복잡해 보일 수 있는 오버레이나 DSL 도입은 장기적으로 중복을 제거하고 휴먼 에러를 막는 가장 확실한 투자입니다.