본문으로 건너뛰기

"Optimizer" 태그 — 2개 게시물

DSPy, 프롬프트 최적화 관련 글

모든 태그 보기

Scaling PostgreSQL to power 800 million ChatGPT users

· 약 9분
최재훈
LEAD (AI Research Engineer), Brain Crew

TL;DR

OpenAI는 ChatGPT와 API를 지원하기 위해 단일 Primary PostgreSQL 인스턴스와 50개의 Read Replica를 활용해 80만 사용자를 지원하며, 연간 10배 이상의 트래픽 증가를 처리하고 있습니다. MVCC(Multi-Version Concurrency Control)의 write amplification 문제를 완화하기 위해 write-heavy 워크로드를 샤딩된 시스템(Azure Cosmos DB)으로 마이그레이션하고, 쿼리 최적화, 워크로드 격리, 연결 풀링, 캐싱 등의 최적화를 통해 PostgreSQL을 초당 수백만 쿼리를 처리하는 규모로 확장했습니다. 이는 적절한 엔지니어링과 최적화를 통해 단일 Primary 아키텍처로도 대규모 read-heavy 워크로드를 충분히 지원할 수 있음을 증명합니다.

Key Takeaways

  • 단일 Primary 아키텍처의 가능성: Read-heavy 워크로드에 대해서는 샤딩 없이도 Primary 하나와 다수의 Read Replica로 대규모 트래픽을 처리할 수 있으며, 이는 샤딩에 수반되는 복잡한 애플리케이션 변경을 피할 수 있게 함
  • MVCC의 근본적 한계 이해: PostgreSQL의 MVCC 구현은 update 시 전체 row를 복사하여 write/read amplification을 유발하고, dead tuple, table bloat, autovacuum 튜닝 등 운영 복잡도를 증가시키므로 write-heavy 워크로드는 다른 시스템으로 분리해야 함
  • 계층화된 트래픽 관리: 우선순위 기반 워크로드 격리, PgBouncer를 통한 연결 풀링, 캐시 락/리스 메커니즘을 통해 트래픽 급증 시 cascading failure를 방지하고 시스템 안정성을 확보할 수 있음
  • 쿼리 최적화의 중요성: ORM이 생성한 다중 테이블 조인(12개 테이블 조인 사례)과 같은 expensive query 하나가 전체 서비스 장애를 유발할 수 있으므로, 복잡한 join은 애플리케이션 레이어로 이동하고 SQL 동작을 엄격히 검토해야 함
  • 단일 장애 지점(SPOF) 완화 전략: 대부분의 critical read를 replica로 오프로드하고, HA 모드로 hot standby를 운영하며, 각 region에 충분한 headroom을 가진 다수의 replica를 배치하여 Primary 장애 시에도 read 서비스는 유지할 수 있도록 설계

상세 내용

OpenAI의 PostgreSQL 스케일링 여정

OpenAI는 수년간 PostgreSQL을 ChatGPT와 API의 핵심 데이터 시스템으로 운영해왔습니다. 사용자 기반이 급격히 증가하면서, 지난 1년간 PostgreSQL 부하는 10배 이상 증가했고 계속해서 빠르게 상승하고 있습니다.

이러한 성장을 지탱하기 위한 프로덕션 인프라 개선 작업을 통해 새로운 인사이트를 얻었습니다: PostgreSQL은 많은 사람들이 생각했던 것보다 훨씬 더 큰 read-heavy 워크로드를 안정적으로 지원할 수 있도록 확장 가능합니다.

현재 OpenAI는 단일 Primary Azure PostgreSQL Flexible Server 인스턴스와 전 세계 여러 region에 분산된 약 50개의 read replica를 통해 8억 명의 사용자를 위한 대규모 글로벌 트래픽을 지원하고 있습니다. Azure Database for PostgreSQL은 완전 관리형 서비스로서 compute와 storage를 분리한 아키텍처를 제공하며, zone redundant 고가용성을 지원하여 동일 Azure region 내 availability zone 간 동기식 복제를 통해 무손실 failover를 가능하게 합니다.

초기 설계의 한계

ChatGPT 출시 후 트래픽이 전례 없는 속도로 증가했습니다. 이를 지원하기 위해 애플리케이션과 PostgreSQL 데이터베이스 레이어 모두에서 광범위한 최적화를 신속히 구현했고, 인스턴스 크기를 늘리는 scale-up과 read replica를 추가하는 scale-out을 수행했습니다.

단일 Primary 아키텍처가 OpenAI 규모의 요구사항을 충족시킬 수 있다는 것은 놀랍게 들릴 수 있지만, 실제로 이를 작동시키는 것은 단순하지 않습니다. PostgreSQL 과부하로 인한 여러 심각한 장애(SEV)를 경험했으며, 이들은 종종 동일한 패턴을 따릅니다:

  • 캐싱 레이어 장애로 인한 광범위한 캐시 미스
  • CPU를 포화시키는 expensive 다중 조인(multi-way join) 급증
  • 새 기능 출시로 인한 write storm

리소스 사용률이 증가하면 쿼리 지연시간이 늘어나고 요청이 타임아웃되기 시작합니다. 재시도는 부하를 더욱 증폭시키고, 전체 ChatGPT 및 API 서비스를 저하시킬 수 있는 악순환을 촉발합니다.

부하 상태에서의 악순환

PostgreSQL MVCC의 문제점

PostgreSQL은 read-heavy 워크로드에 대해 잘 확장되지만, write 트래픽이 많은 기간에는 여전히 어려움을 겪습니다. 이는 주로 PostgreSQL의 MVCC(Multi-Version Concurrency Control) 구현 때문입니다.

MVCC의 기본 개념은 DBMS가 여러 쿼리가 가능한 한 서로 간섭 없이 동시에 데이터베이스에 읽고 쓸 수 있도록 하는 것입니다. 쿼리가 실행될 때 DBMS는 트랜잭션이 시작된 시점의 데이터베이스 스냅샷을 관찰합니다(snapshot isolation). 이 접근 방식은 reader가 데이터에 접근하는 것을 차단하는 명시적인 레코드 락의 필요성을 제거합니다.

그러나 PostgreSQL의 MVCC 구현에는 심각한 문제가 있습니다:

Write Amplification: 쿼리가 tuple을 업데이트하거나 단일 필드만 수정할 때도 전체 row를 복사하여 새 버전을 생성합니다. Write가 많은 워크로드에서는 상당한 write amplification이 발생합니다.

Read Amplification: 쿼리가 최신 버전을 검색하기 위해 여러 tuple 버전(dead tuple)을 스캔해야 하므로 read amplification도 증가합니다.

운영 복잡도: Table과 index bloat, index 유지관리 오버헤드 증가, 복잡한 autovacuum 튜닝 등 추가적인 문제를 야기합니다.

Carnegie Mellon University의 Andy Pavlo 교수와 함께 작성한 블로그 "The Part of PostgreSQL We Hate the Most"에서 이러한 이슈에 대한 심층 분석을 제공하고 있으며, 이는 PostgreSQL Wikipedia 페이지에서도 인용되고 있습니다. 이 글에서는 PostgreSQL의 MVCC 구현이 MySQL, Oracle, Microsoft SQL Server를 포함한 다른 주요 관계형 DBMS 중 최악이라고 지적합니다.

초당 수백만 쿼리로 PostgreSQL 확장하기

이러한 한계를 완화하고 write 압력을 줄이기 위해 다음과 같은 전략을 채택했습니다:

Write 워크로드 마이그레이션

샤딩 가능한(수평 파티셔닝 가능한) write-heavy 워크로드를 Azure Cosmos DB와 같은 샤딩된 시스템으로 마이그레이션하고, 불필요한 write를 최소화하도록 애플리케이션 로직을 최적화했습니다. 또한 현재 PostgreSQL 배포에 새로운 테이블 추가를 더 이상 허용하지 않으며, 새 워크로드는 기본적으로 샤딩된 시스템을 사용합니다.

현재 인프라가 발전했음에도 PostgreSQL은 샤딩되지 않은 상태로, 단일 Primary 인스턴스가 모든 write를 처리합니다. 주된 이유는 기존 애플리케이션 워크로드를 샤딩하는 것이 매우 복잡하고 시간이 많이 걸리며, 수백 개의 애플리케이션 엔드포인트를 변경해야 하고 몇 달 또는 몇 년이 걸릴 수 있기 때문입니다. 워크로드가 주로 read-heavy이고 광범위한 최적화를 구현했기 때문에, 현재 아키텍처는 여전히 트래픽 증가를 지원할 충분한 여유를 제공합니다.

Primary 부하 감소

과제: 단일 writer만 있는 경우 write를 확장할 수 없습니다. write 급증은 Primary를 빠르게 과부하시켜 ChatGPT 및 API와 같은 서비스에 영향을 줄 수 있습니다.

솔루션: Primary에서 read와 write 모두 가능한 한 부하를 최소화하여 write 급증을 처리할 충분한 용량을 확보합니다. Read 트래픽은 가능한 한 replica로 오프로드됩니다. 그러나 write 트랜잭션의 일부인 일부 read 쿼리는 Primary에 남아야 합니다. 이러한 경우 쿼리가 효율적이고 느린 쿼리를 피하도록 보장하는 데 집중합니다.

쿼리 최적화

과제: PostgreSQL에서 여러 expensive 쿼리를 식별했습니다. 과거에는 이러한 쿼리의 볼륨 급증이 대량의 CPU를 소비하여 ChatGPT와 API 요청을 모두 느리게 만들었습니다.

솔루션: 12개 테이블을 조인하는 매우 비용이 많이 드는 쿼리를 발견했으며, 이 쿼리의 급증이 과거 고심각도 SEV의 원인이었습니다. 복잡한 다중 테이블 조인은 가능한 한 피해야 합니다. 조인이 필요한 경우 쿼리를 분해하고 복잡한 조인 로직을 애플리케이션 레이어로 이동하는 것을 고려해야 합니다.

이러한 문제 쿼리 중 다수는 ORM(Object-Relational Mapping) 프레임워크에 의해 생성되므로, 생성된 SQL을 주의 깊게 검토하고 예상대로 동작하는지 확인하는 것이 중요합니다. 또한 idle_in_transaction_session_timeout과 같은 timeout을 구성하여 장기 실행 idle 쿼리가 autovacuum을 차단하는 것을 방지해야 합니다.

단일 장애 지점(SPOF) 완화

과제: Read replica가 다운되면 트래픽을 다른 replica로 라우팅할 수 있습니다. 그러나 단일 writer에 의존한다는 것은 단일 장애 지점이 있다는 의미이며, 다운되면 전체 서비스가 영향을 받습니다.

솔루션: 가장 중요한 요청은 read 쿼리만 포함합니다. Primary의 단일 장애 지점을 완화하기 위해 writer에서 replica로 이러한 read를 오프로드하여 Primary가 다운되어도 해당 요청이 계속 서비스될 수 있도록 합니다.

Primary 장애를 완화하기 위해 Hot Standby와 함께 고가용성(HA) 모드로 Primary를 실행합니다. Hot Standby는 지속적으로 동기화되는 replica로 항상 트래픽을 인계받을 준비가 되어 있습니다. PostgreSQL에서는 Primary 서버가 continuous archiving 모드로 작동하고 각 standby 서버는 continuous recovery 모드로 작동하며 Primary에서 WAL 파일을 읽습니다. Azure PostgreSQL 팀은 매우 높은 부하에서도 이러한 failover가 안전하고 안정적으로 유지되도록 상당한 작업을 수행했습니다.

워크로드 격리

과제: 특정 요청이 PostgreSQL 인스턴스에서 불균형적으로 많은 리소스를 소비하는 상황이 자주 발생합니다. 이는 동일한 인스턴스에서 실행되는 다른 워크로드의 성능 저하로 이어질 수 있습니다.

솔루션: "noisy neighbor" 문제를 완화하기 위해 워크로드를 전용 인스턴스로 격리하여 리소스 집약적 요청의 급증이 다른 트래픽에 영향을 주지 않도록 합니다. 구체적으로 요청을 low-priority와 high-priority tier로 분할하고 별도의 인스턴스로 라우팅합니다. 이렇게 하면 low-priority 워크로드가 리소스 집약적이 되어도 high-priority 요청의 성능이 저하되지 않습니다.

연결 풀링(Connection Pooling)

과제: 각 인스턴스에는 최대 연결 제한이 있습니다(Azure PostgreSQL에서 5,000개). 연결이 부족하거나 idle 연결이 너무 많이 누적되기 쉽습니다. 이전에 모든 사용 가능한 연결을 소진시킨 connection storm으로 인한 장애가 있었습니다.

솔루션: PgBouncer를 프록시 레이어로 배포하여 데이터베이스 연결을 풀링합니다. Statement 또는 transaction 풀링 모드로 실행하면 연결을 효율적으로 재사용하여 활성 클라이언트 연결 수를 크게 줄일 수 있습니다. 또한 연결 설정 지연시간을 줄입니다: 벤치마크에서 평균 연결 시간이 50밀리초에서 5밀리초로 감소했습니다.

Region 간 연결과 요청은 비용이 많이 들 수 있으므로 프록시, 클라이언트, replica를 동일한 region에 배치하여 네트워크 오버헤드와 연결 사용 시간을 최소화합니다.

각 read replica에는 여러 PgBouncer 파드를 실행하는 자체 Kubernetes 배포가 있습니다. 동일한 Kubernetes Service 뒤에서 여러 Kubernetes 배포를 실행하여 파드 간 트래픽을 로드 밸런싱합니다.

PostgreSQL 프록시로서의 PgBouncer

캐싱 전략

과제: 캐시 미스의 급증은 PostgreSQL 데이터베이스에 read 급증을 유발하여 CPU를 포화시키고 사용자 요청을 느리게 만들 수 있습니다.

솔루션: PostgreSQL의 read 압력을 줄이기 위해 캐싱 레이어를 사용하여 대부분의 read 트래픽을 제공합니다. 그러나 캐시 hit rate가 예기치 않게 떨어지면 캐시 미스의 burst가 대량의 요청을 직접 PostgreSQL로 푸시할 수 있습니다.

캐시 미스 storm 동안 과부하를 방지하기 위해 캐시 락킹(및 리싱) 메커니즘을 구현하여 특정 키에 대해 미스가 발생한 단일 reader만 PostgreSQL에서 데이터를 가져오도록 합니다. 여러 요청이 동일한 캐시 키에 대해 미스가 발생하면 한 요청만 락을 획득하고 다른 요청은 해당 요청이 데이터를 가져올 때까지 대기합니다.

결론

OpenAI의 경험은 적절한 최적화와 엔지니어링을 통해 PostgreSQL을 초당 수백만 쿼리를 처리하고 수억 명의 사용자를 지원하는 규모로 확장할 수 있음을 보여줍니다. MVCC의 근본적인 한계를 이해하고 이를 완화하기 위한 전략적 접근(write 워크로드 분리, 쿼리 최적화, 워크로드 격리, 연결 풀링, 캐싱)을 통해 단일 Primary 아키텍처의 한계를 극복하고 안정적인 서비스를 제공할 수 있었습니다.

References

DSPy를 사용한 LLM 최적화: AI 시스템 구축, 최적화 및 평가를 위한 단계별 가이드

· 약 6분
김성연
AI Research Engineer, Brain Crew

TL;DR

DSPy는 프롬프트 엔지니어링을 프로그래밍으로 전환하는 Stanford NLP의 프레임워크입니다. 수동 프롬프트 작성 대신 Python 코드로 LM의 동작을 정의하고, BootstrapFewShot, MIPRO 등의 옵티마이저를 통해 프롬프트와 가중치를 자동으로 최적화합니다. 모듈과 시그니처를 조합해 AI 파이프라인을 구축하고, 메트릭 기반으로 평가하여 반복 개선할 수 있어 RAG, 에이전트, 복잡한 추론 시스템 구축에 적합합니다.

Key Takeaways

  • 프롬프트를 코드로 관리: 문자열 기반 프롬프팅 대신 dspy.Signaturedspy.Module로 AI 동작을 선언적으로 정의하여 유지보수성과 재사용성 향상
  • 자동 최적화: BootstrapFewShot, MIPRO, BootstrapFinetune 등의 옵티마이저가 메트릭을 기준으로 프롬프트와 few-shot 예제를 자동으로 튜닝
  • 메트릭 중심 평가: 커스텀 메트릭을 정의하여 정량적으로 성능을 측정하고, 옵티마이저가 이를 기반으로 개선 방향 결정
  • 모듈화 아키텍처: ChainOfThought, ReAct 등 사전 구축된 모듈과 어댑터를 조합하여 복잡한 멀티스텝 파이프라인 구축 가능
  • 프로덕션 지원: 캐싱, 스트리밍, async, 옵저버빌리티 등 실전 배포에 필요한 기능 제공 및 MCP 통합 지원

상세 내용

DSPy란 무엇인가

DSPy(Declarative Self-improving Language Programs, in Python)는 Stanford NLP에서 개발한 LM(Language Model) 프로그래밍 프레임워크입니다. 전통적인 프롬프트 엔지니어링의 한계를 극복하기 위해, 프롬프트를 문자열로 작성하는 대신 Python 코드로 LM의 동작을 정의하고 자동으로 최적화할 수 있도록 설계되었습니다.

기존 LLM 애플리케이션 개발에서는 모델을 변경하거나 파이프라인이 복잡해질 때마다 프롬프트를 수동으로 재작성해야 했습니다. DSPy는 이러한 반복 작업을 프로그래밍 추상화와 자동 최적화를 통해 해결합니다.

DSPy Overview

DSPy의 핵심 구성 요소

1. Modules: AI 동작을 코드로 정의

DSPy의 모듈은 LM 호출을 추상화한 재사용 가능한 컴포넌트입니다. 프롬프트를 직접 작성하는 대신, 입력과 출력의 시그니처(Signature)를 정의하면 DSPy가 자동으로 적절한 프롬프트를 생성합니다.

주요 내장 모듈:

  • dspy.ChainOfThought: 단계별 추론을 수행하는 모듈
  • dspy.ReAct: 추론과 행동을 반복하는 에이전트 패턴
  • dspy.Predict: 기본적인 입력-출력 예측 모듈

예시 코드:

import dspy

# Signature 정의: 입력(question) → 출력(answer)
class BasicQA(dspy.Signature):
"""Answer questions with short factoid answers."""
question = dspy.InputField()
answer = dspy.OutputField(desc="often between 1 and 5 words")

# Module 생성
generate_answer = dspy.ChainOfThought(BasicQA)

2. Optimizers: 프롬프트와 가중치의 자동 튜닝

DSPy의 가장 강력한 기능은 옵티마이저입니다. 메트릭을 기준으로 프롬프트, few-shot 예제, 심지어 LM의 가중치까지 자동으로 최적화합니다.

주요 옵티마이저:

  • BootstrapFewShot: 학습 데이터에서 효과적인 few-shot 예제를 자동 선택
  • BootstrapFewShotWithRandomSearch: 랜덤 탐색을 추가하여 더 나은 예제 조합 탐색
  • MIPRO: 프롬프트 인스트럭션과 few-shot 예제를 동시에 최적화하는 고급 기법
  • BootstrapFinetune: DSPy 파이프라인을 활용해 소형 LM을 파인튜닝

3. Evaluation: 메트릭 기반 성능 측정

DSPy는 사용자 정의 메트릭을 통해 AI 시스템의 성능을 정량적으로 평가할 수 있습니다. 옵티마이저는 이 메트릭을 목표 함수로 사용하여 최적화를 수행합니다.

# 메트릭 정의 예시
def validate_answer(example, pred, trace=None):
answer_correct = example.answer.lower() == pred.answer.lower()
return answer_correct

DSPy 시작하기: 단계별 가이드

Step 1: 설치 및 환경 설정

pip install dspy-ai

DSPy는 다양한 LM과 Retrieval Model(RM)을 지원합니다. 초기 설정 예시:

import dspy

# Language Model 설정
turbo = dspy.OpenAI(model='gpt-3.5-turbo')

# Retrieval Model 설정 (옵션)
colbertv2_wiki17_abstracts = dspy.ColBERTv2(
url='http://20.102.90.50:2017/wiki17_abstracts'
)

# 전역 설정
dspy.settings.configure(lm=turbo, rm=colbertv2_wiki17_abstracts)

Step 2: 데이터셋 준비

DSPy는 HotPotQA 등의 벤치마크 데이터셋을 내장 지원합니다:

from dspy.datasets import HotPotQA

# train_seed로 재현성 보장
dataset = HotPotQA(
train_seed=1,
train_size=20,
eval_size=100
)

trainset = dataset.train
devset = dataset.dev

Step 3: 파이프라인 구축

DSPy 모듈을 조합하여 복잡한 AI 파이프라인을 구성할 수 있습니다:

class RAG(dspy.Module):
def __init__(self, num_passages=3):
super().__init__()
self.retrieve = dspy.Retrieve(k=num_passages)
self.generate_answer = dspy.ChainOfThought(BasicQA)

def forward(self, question):
context = self.retrieve(question).passages
prediction = self.generate_answer(context=context, question=question)
return dspy.Prediction(context=context, answer=prediction.answer)

Step 4: 최적화 실행

메트릭을 정의하고 옵티마이저를 실행하여 파이프라인을 개선합니다:

from dspy.teleprompt import BootstrapFewShot

# 메트릭 정의
def validate_context_and_answer(example, pred, trace=None):
answer_match = example.answer.lower() == pred.answer.lower()
context_relevant = any(example.answer.lower() in c.lower()
for c in pred.context)
return answer_match and context_relevant

# 옵티마이저 설정 및 실행
optimizer = BootstrapFewShot(metric=validate_context_and_answer)
optimized_rag = optimizer.compile(RAG(), trainset=trainset)

Step 5: 평가 및 배포

최적화된 모델을 평가하고 저장합니다:

from dspy.evaluate import Evaluate

# 평가 실행
evaluator = Evaluate(devset=devset, metric=validate_context_and_answer,
num_threads=4, display_progress=True)
score = evaluator(optimized_rag)

# 모델 저장
optimized_rag.save('optimized_rag.json')

실전 활용 사례

Retrieval-Augmented Generation (RAG)

DSPy는 RAG 시스템 구축에 특히 효과적입니다. Retrieval과 Generation을 별도 모듈로 분리하여 각각 최적화할 수 있으며, 멀티홉 추론이 필요한 복잡한 질의응답에도 대응 가능합니다.

AI 에이전트 구축

ReAct 패턴을 활용한 에이전트 개발을 지원하며, MCP(Model Context Protocol) 통합을 통해 외부 도구와의 연동도 가능합니다. 메모리 기능을 추가한 대화형 에이전트, 금융 분석 에이전트 등의 실사례가 제공됩니다.

Entity Extraction 및 Classification

구조화된 정보 추출 작업에도 DSPy를 활용할 수 있습니다. 시그니처로 출력 스키마를 정의하고, 옵티마이저가 최적의 프롬프트를 찾아내도록 할 수 있습니다.

고급 최적화 기법

GEPA (Reflective Prompt Evolution)

GEPA(Generalized Evolutionary Prompt Adaptation)는 프롬프트를 반성적으로 진화시키는 실험적 기법입니다. AIME 수학 문제, 기업용 정보 추출, 코드 백도어 분류 등 복잡한 태스크에서 효과를 입증했습니다.

RL 기반 최적화

실험적으로 강화학습을 활용한 최적화도 지원하며, 프라이버시 중심 위임(Privacy-Conscious Delegation)이나 멀티홉 리서치 등의 고급 유스케이스에 적용 가능합니다.

프로덕션 배포 고려사항

DSPy는 실전 배포를 위한 다양한 기능을 제공합니다:

  • 캐싱: 반복 호출 시 비용 절감
  • 스트리밍 및 Async: 실시간 응답성 개선
  • 디버깅 & 옵저버빌리티: DSPy 옵티마이저 추적 및 모니터링
  • 모델 저장/로드: 최적화된 파이프라인의 버전 관리

DSPy 에코시스템

DSPy는 활발한 오픈소스 커뮤니티를 보유하고 있으며(GitHub 33.2k+ stars), 지속적으로 새로운 튜토리얼과 사례가 추가되고 있습니다. Audio 처리, 이미지 생성, Code Generation 등 멀티모달 태스크로도 확장되고 있습니다.

언제 DSPy를 사용해야 하나

DSPy는 다음과 같은 상황에서 특히 유용합니다:

  • 여러 LM 호출이 연결된 복잡한 파이프라인을 구축할 때
  • 프롬프트를 체계적으로 관리하고 버전 관리가 필요할 때
  • 메트릭 기반으로 자동 최적화하여 수동 튜닝 시간을 줄이고 싶을 때
  • 다양한 모델(GPT-4, Claude, 로컬 모델 등)로 실험하며 최적 조합을 찾을 때
  • 프로덕션 환경에서 성능과 비용을 지속적으로 개선해야 할 때

반면, 단순한 단일 프롬프트 호출이나 프로토타이핑 초기 단계에서는 오버엔지니어링이 될 수 있습니다.

References