ACID 보장이 데이터베이스 설계와 애플리케이션 동작에 어떤 영향을 미치는지 알아보세요. 원자성, 일관성, 격리성, 내구성, 트레이드오프와 실제 사례를 살펴봅니다.

장보기 결제이든, 항공편 예약이든, 계좌 간 이체든 결과가 명확하길 기대합니다: 성공했거나 실패했거나. 데이터베이스도 많은 사용자가 동시에 쓰고, 서버가 죽고, 네트워크가 불안정한 상황에서도 같은 확실성을 제공하려 합니다.
트랜잭션은 데이터베이스가 하나의 "패키지"로 취급하는 작업 단위입니다. 재고 차감, 주문 레코드 생성, 카드 청구, 영수증 기록 등 여러 단계를 포함할 수 있지만 하나의 일관된 동작처럼 행동해야 합니다.
어떤 단계라도 실패하면 시스템은 반쯤 끝난 상태를 남기기보다 안전한 지점으로 되돌려야 합니다.
부분 업데이트는 단순한 기술적 오류가 아니라 고객 지원 티켓과 재무적 위험으로 이어집니다. 예를 들어:
이런 실패는 디버그하기가 어렵습니다. 모든 것이 "대부분 맞는 것처럼" 보이지만 숫자가 맞지 않습니다.
ACID는 많은 데이터베이스가 트랜잭션에 대해 제공할 수 있는 네 가지 보장을 줄여 부르는 말입니다:
ACID는 특정 브랜드나 토글 가능한 단일 기능이 아닙니다. 동작에 대한 약속입니다.
강한 보장은 일반적으로 데이터베이스가 더 많은 작업을 해야 함을 의미합니다: 추가 조정, 잠금 대기, 버전 추적, 로그 기록 등. 과도한 경우 처리량이 떨어지거나 대기 시간이 늘어날 수 있습니다. 목표는 항상 "항상 최대 ACID"가 아니라 실제 비즈니스 위험에 맞는 보장을 선택하는 것입니다.
원자성은 트랜잭션을 하나의 작업 단위로 다루어, 완전히 끝나거나 전혀 영향이 없도록 합니다. 데이터베이스에서 "반만 업데이트된" 상태가 보이지 않습니다.
Alice에서 Bob으로 $50를 이체한다고 가정하면 내부적으로 적어도 두 가지 변경이 필요합니다:
원자성이 있으면 두 변경이 함께 성공하거나 함께 실패합니다. 둘 다 안전하게 할 수 없다면 둘 다 하지 않아야 합니다. 이로써 Alice만 차감되고 Bob에게는 돈이 가지 않는 최악의 상황을 막습니다.
데이터베이스는 트랜잭션에 두 가지 출구를 제공합니다:
유용한 비유는 "초안 대 공개"입니다. 트랜잭션이 실행되는 동안 변경은 임시적입니다. 커밋만이 이를 공개합니다.
원자성이 중요한 이유는 실패가 일상적이기 때문입니다:
이런 일이 커밋 전에 발생하면 원자성은 데이터베이스가 롤백하여 부분적인 작업이 실제 잔액으로 유출되지 않도록 보장합니다.
원자성은 DB 상태를 보호하지만, 네트워크 단절로 커밋 여부가 불분명할 때 애플리케이션은 여전히 불확실성을 처리해야 합니다.
두 가지 실무 보완책:
원자적 트랜잭션과 멱등한 재시도를 함께 쓰면 부분 업데이트와 실수로 인한 중복 청구를 모두 피할 수 있습니다.
ACID에서의 일관성은 "데이터가 그럴듯해 보인다"거나 "모든 복제본이 일치한다"는 뜻이 아닙니다. 트랜잭션은 당신이 정의한 규칙에 따라 데이터베이스를 한 유효한 상태에서 다른 유효한 상태로만 이동시켜야 한다는 뜻입니다.
데이터베이스는 제약, 트리거, 불변식으로 표현된 명시적 규칙을 기준으로만 데이터를 일관되게 유지할 수 있습니다. ACID는 이 규칙들을 트랜잭션 동안 강제합니다.
일반적인 예:
order.customer_id 는 존재하는 고객을 가리켜야 함이 규칙들이 있으면 데이터베이스는 이를 위반하는 트랜잭션을 거부하므로 “반쪽 유효한” 데이터가 나오지 않습니다.
앱 수준 검증은 사용자 경험을 개선하고 복잡한 비즈니스 규칙을 적용하는 데 중요하지만 단독으로는 충분하지 않습니다.
고전적 실패모드는 앱에서 "이메일 사용 가능"을 확인한 뒤 행을 삽입할 때 두 요청이 동시에 확인을 통과하는 경우입니다. 데이터베이스의 고유 제약이 한 번만 삽입되도록 보장합니다.
"잔액은 음수가 될 수 없다"를 제약이나 단일 트랜잭션 내에서 안정적으로 강제하면, 초과 인출을 초래하는 전송은 전체가 실패해야 합니다. 규칙을 아무 데도 명시하지 않으면 ACID도 보호할 수 없습니다—강제할 수단이 없기 때문입니다.
일관성은 결국 명확성을 요구합니다: 규칙을 정의하고, 트랜잭션이 그 규칙을 결코 깨지 않도록 하세요.
격리성은 트랜잭션들이 서로 간섭하지 않도록 합니다. 한 트랜잭션이 진행되는 동안 다른 트랜잭션은 반쯤 끝난 작업을 보거나 실수로 덮어쓰지 않아야 합니다. 목표는 간단합니다: 많은 사용자가 활동하더라도 각 트랜잭션이 마치 혼자 실행된 것처럼 행동해야 합니다.
실제 시스템은 바쁩니다: 고객 주문, 지원 담당자 프로필 수정, 백그라운드 결제 정산 작업 등이 동시에 일어납니다. 이런 작업들은 시간이 겹치고 종종 같은 행(계좌 잔액, 재고 수량, 예약 슬롯)에 접근합니다.
격리성이 없으면 타이밍이 비즈니스 로직의 일부가 됩니다. "재고 차감" 업데이트가 다른 체크아웃과 경쟁하거나, 보고서가 변경 도중의 데이터를 읽어 존재하지 않는 수치를 보여줄 수 있습니다.
완전한 "혼자 실행한 것처럼" 격리하면 비용이 높습니다. 처리량이 줄고 대기(락)가 늘거나 트랜잭션 재시도가 발생할 수 있습니다. 많은 워크플로우는 가장 엄격한 보호가 필요하지 않습니다(예: 어제의 분석을 읽는 작업은 약간의 불일치를 허용할 수 있음).
그래서 데이터베이스는 선택 가능한 격리 수준을 제공합니다: 어느 정도의 동시성 위험을 감수하고 성능/충돌을 줄일지 선택하세요.
격리가 너무 약하면 다음과 같은 고전적 이상이 발생합니다:
이러한 실패 모드를 이해하면 제품의 약속에 맞는 격리 수준을 고르기 쉽습니다.
격리는 트랜잭션이 다른 트랜잭션을 실행 중에 무엇을 "볼 수 있는지"를 결정합니다. 격리가 약하면 이상(사용자에게 놀라운 동작)이 발생할 수 있습니다.
더티 리드는 다른 트랜잭션이 쓴(하지만 커밋하지 않은) 데이터를 읽을 때 발생합니다.
시나리오: Alex가 계좌에서 $500를 이체해 잔액이 임시로 $200가 되었고, 당신이 Alex의 이체가 실패해 롤백되기 전 그 $200를 읽음.
사용자 관점: 고객이 잘못된 낮은 잔액을 보거나, 사기 탐지 규칙이 잘못 동작하거나, 지원 담당자가 잘못된 정보를 제공함.
**비반복 가능 리드(Non-repeatable read)**는 같은 행을 두 번 읽었을 때 다른 값을 얻는 경우입니다.
시나리오: 주문 합계 $49.00을 읽고 잠깐 후 새로고침했더니 할인 항목이 제거되어 $54.00으로 바뀜.
사용자 관점: "결제 중 내 총액이 바뀌었다"는 불신으로 연결될 수 있습니다.
**팬텀 리드(Phantom read)**는 비슷하지만 행 집합 자체가 달라지는 경우입니다: 다른 트랜잭션이 매칭되는 레코드를 삽입/삭제함.
시나리오: 호텔 검색에서 "3개 이용 가능"으로 표시되었다가 예약 과정에서 다시 확인하니 없음(다른 예약이 들어옴).
사용자 관점: 예약 중복이나 일관성 없는 가용성 화면, 과판매 발생.
로스트 업데이트는 두 트랜잭션이 같은 값을 읽고 각각 업데이트를 쓰는데, 나중 쓰기가 앞선 변경을 덮어써서 하나의 변경이 사라지는 경우입니다.
시나리오: 두 관리자 모두 같은 상품 가격을 편집. 둘 다 $10에서 시작해 한 명은 $12로 저장, 다른 한 명은 나중에 $11로 저장해 앞선 변경이 사라짐.
**라이트 스큐(Write skew)**는 두 트랜잭션이 각각 개별적으로는 유효한 변경을 하지만 둘을 합치면 규칙을 위반하는 경우입니다.
시나리오: 규칙: "항상 최소 한 명의 당직 의사가 배치되어 있어야 한다." 두 의사가 서로 상대가 당직인 것을 확인하고 동시에 오프를 표시하면 함께 적용되어 결과적으로 당직자가 0명이 됨.
강한 격리는 이상을 줄이지만 대기, 재시도, 비용을 증가시킵니다. 읽기 중심의 분석에는 약한 격리가 적절할 수 있고, 돈 이동이나 예약처럼 정확성이 중요한 흐름에는 더 엄격한 설정을 쓰는 것이 낫습니다.
격리는 트랜잭션이 다른 트랜잭션이 실행되는 동안 무엇을 "볼 수 있는지"에 관한 것입니다. DB는 이를 격리 수준으로 노출합니다. 높은 수준은 놀라운 동작을 줄이지만 처리량과 응답성에 비용을 지불해야 합니다.
많은 팀은 사용자 대상 앱에서 기본으로 Read Committed를 선택합니다: 성능이 좋고 "더티 리드 없음"은 대부분의 기대에 부합합니다.
Read Uncommitted는 OLTP 시스템에서는 드뭅니다. 모니터링이나 대략적인 리포팅에 간혹 쓰입니다.
이름은 표준화되어 있지만 정확한 보장은 DB 엔진마다 다릅니다(때로는 설정에 따라 달라짐). 문서를 확인하고 비즈니스에 중요한 이상을 테스트하세요.
내구성은 트랜잭션이 커밋되면 그 결과가 크래시(전원 손실, 프로세스 재시작, 머신 재부팅)를 견뎌야 한다는 의미입니다. 앱이 고객에게 "결제 성공"이라고 말하면, 내구성은 DB가 다음 장애 이후에도 그 사실을 잊지 않을 것이라는 약속입니다.
대부분의 관계형 DB는 **WAL(Write-Ahead Logging)**으로 내구성을 달성합니다. 요약하면, DB는 트랜잭션을 커밋으로 간주하기 전에 변경의 순차적 "영수증"을 디스크의 로그에 씁니다. DB가 크래시하면 시작 시 로그를 재생해 커밋된 변경을 복원합니다.
복구 시간을 합리적으로 유지하기 위해 DB는 또한 체크포인트를 만듭니다. 체크포인트는 최근 변경 중 충분한 부분을 메인 데이터 파일에 써서 복구 시 재생해야 하는 로그 양을 제한합니다.
내구성은 단순한 on/off가 아닙니다. 데이터가 안정적 저장소에 얼마나 강제로 기록되는지에 달려 있습니다.
fsync 호출)될 때까지 기다린 뒤 커밋을 응답합니다. 안전하지만 지연이 늘어납니다.기본 하드웨어도 중요합니다: SSD, RAID 컨트롤러의 쓰기 캐시, 클라우드 볼륨은 장애 시 다르게 동작할 수 있습니다.
백업과 복제는 복구와 가용성 축소에 도움을 주지만, 내구성과 동일하지 않습니다. 트랜잭션이 주(primary)에 내구적일 수 있지만 복제본에 아직 도달하지 않았을 수 있고, 백업은 보통 시점별 스냅샷이지 커밋 단위 보장은 아닙니다.
BEGIN으로 트랜잭션을 시작하고 나중에 COMMIT할 때, DB는 누가 어떤 행을 읽고 쓸 수 있는지, 동시에 누가 같은 레코드를 변경하려고 할 때 무슨 일이 일어나는지 등을 조정합니다.
충돌을 다루는 핵심 선택은 다음과 같습니다:
많은 시스템은 워크로드와 격리 수준에 따라 두 접근을 혼합합니다.
현대 DB는 종종 **MVCC(다중 버전 동시성 제어)**를 사용합니다. 한 행의 단일 복사본만 유지하는 대신 여러 버전을 유지합니다.
이 덕분에 일부 DB는 많은 읽기와 쓰기를 동시에 처리하면서 블로킹을 줄일 수 있습니다. 다만 쓰기-쓰기 충돌은 여전히 해결해야 합니다.
잠금은 데드락을 유발할 수 있습니다: 트랜잭션 A가 B가 가진 잠금을 기다리고 있고 B는 A가 가진 잠금을 기다리는 상황. DB는 보통 사이클을 감지해 한 트랜잭션을 중단(데드락 희생자) 시키고 애플리케이션이 재시도하도록 오류를 반환합니다.
ACID 강제가 마찰을 일으키면 보통 다음을 관찰합니다:
이 신호들은 트랜잭션 크기, 인덱스, 또는 어떤 격리/잠금 전략이 적합한지 재검토해야 할 때임을 알려줍니다.
ACID 보장은 단순한 데이터베이스 이론이 아니라 API, 백그라운드 작업, UI 흐름 설계에 영향을 줍니다. 핵심 아이디어는 어떤 단계들이 함께 성공해야 하는지 결정하고, 그 단계들만 트랜잭션으로 감싸는 것입니다.
좋은 트랜잭션 API는 보통 하나의 비즈니스 동작에 매핑됩니다. 예를 들어 /checkout 작업은 주문 생성, 재고 예약, 결제 의도 기록을 포함할 수 있습니다. 이러한 DB 쓰기들은 보통 하나의 트랜잭션에 넣어 검증 실패 시 함께 롤백되도록 합니다.
일반 패턴:
이렇게 하면 원자성과 일관성을 지키면서 느리고 취약한 트랜잭션을 피할 수 있습니다.
트랜잭션 경계를 어디에 두느냐는 "하나의 작업 단위"가 무엇인지에 달려 있습니다:
ACID가 돕지만 애플리케이션은 여전히 실패를 올바르게 처리해야 합니다:
긴 트랜잭션, 트랜잭션 안에서 외부 API 호출, 사용자 생각 시간 동안의 잠금(예: 장바구니 행을 잠근 채 사용자 확인 대기)은 피하세요. 이들은 경합을 키우고 격리 충돌을 훨씬 더 자주 발생시킵니다.
빠르게 트랜잭션 시스템을 만들 때 가장 큰 위험은 보통 ACID를 몰라서가 아니라 하나의 비즈니스 동작을 여러 엔드포인트, 작업, 테이블에 흩어놓아 트랜잭션 경계를 명확히 하지 않는 것입니다.
예: Koder.ai 같은 플랫폼은 ACID를 중심으로 설계하면서도 개발 속도를 높이는 데 도움을 줄 수 있습니다. 워크플로(예: "재고 예약과 결제 의도를 포함한 체크아웃")를 설명하면 React UI와 Go + PostgreSQL 백엔드를 생성하고 스냅샷/롤백으로 스키마나 트랜잭션 경계를 바꾸며 반복할 수 있게 해 줍니다. 데이터베이스는 여전히 보장을 강제하고, 도구의 가치는 올바른 설계에서 작동하는 구현으로 가는 경로를 단축하는 데 있습니다.
단일 데이터베이스는 보통 하나의 트랜잭션 경계 내에서 ACID 보장을 제공할 수 있습니다. 작업을 여러 서비스(종종 여러 DB)에 분산하면 같은 보장을 유지하기가 더 어렵고, 시도하면 비용이 커집니다.
엄격한 일관성은 모든 읽기가 "최신의 커밋된 진실"을 보게 하는 것입니다. 높은 가용성은 일부 구성 요소가 느리거나 도달 불가능할 때도 시스템이 응답을 계속하도록 하는 것입니다.
다중 서비스 구성에서는 임시 네트워크 문제로 선택을 강요당할 수 있습니다: 모든 참가자가 동의할 때까지 요청을 블록하거나 실패시키기(더 일관성 있지만 덜 가용함), 아니면 서비스들이 잠깐 불일치해도 요청을 허용하기(더 가용하지만 덜 일관성 있음). 어느 쪽도 항상 옳지 않습니다—비즈니스가 어떤 실수를 감내할 수 있는지에 달려 있습니다.
분산 트랜잭션은 네트워크 지연, 재시도, 타임아웃, 서비스 크래시, 부분 실패 등 당신이 완전히 통제할 수 없는 경계들 간의 조정을 필요로 합니다.
모든 서비스가 올바르더라도 네트워크 때문에 모호성이 생길 수 있습니다: 결제 서비스는 커밋했지만 주문 서비스가 확인을 받지 못했을 수 있습니다. 이를 안전하게 해결하려면 2단계 커밋 같은 조정 프로토콜이 필요하고, 이는 느리고 장애 시 가용성을 낮추며 운영 복잡성을 더합니다.
사가(Saga): 워크플로를 단계로 나누고 각 단계는 로컬 커밋. 이후 단계가 실패하면 앞선 단계들을 보상(예: 환불)으로 되돌림.
아웃박스/인박스: 서비스는 비즈니스 데이터와 "발행할 이벤트" 레코드를 같은 로컬 트랜잭션에 씁니다(아웃박스). 소비자는 처리된 메시지 ID를 기록(인박스)해 재시도 시 중복 효과를 방지합니다.
최종적 일관성(Eventual consistency): 서비스 간 데이터가 잠시 다를 수 있음을 허용하고 조정을 위한 계획을 둠.
보장을 완화할 때:
위험 통제 방법:
정말 중요한 불변식(예: "계좌를 절대 초과 출금하지 않음")은 가능하면 하나의 서비스와 하나의 DB 트랜잭션 내에 유지하세요.
유닛 테스트에서 "정상"인 트랜잭션도 실제 트래픽, 재시작, 동시성에서는 실패할 수 있습니다. ACID 보장이 운영 환경에서 기대대로 작동하도록 다음 체크리스트를 사용하세요.
먼저 항상 참이어야 하는 것을 적으세요(데이터 불변식). 예: "계좌 잔액은 음수가 될 수 없다", "주문 합계는 라인 아이템 합과 같다", "재고는 0 아래로 내려갈 수 없다", "결제는 정확히 하나의 주문에 연결된다". 이들을 제품 규칙으로 취급하세요.
그다음 어떤 것이 반드시 하나의 트랜잭션 안에 있어야 하고 어떤 것은 지연 가능한지 결정하세요.
트랜잭션을 작게 유지하세요: 적은 행을 건드리고, 외부 API 호출을 포함하지 말고, 빠르게 커밋하세요.
동시성을 테스트의 주요 축으로 만드세요.
재시도를 지원하면 명시적 멱등성 키를 추가하고 "성공 후 요청 반복" 시나리오도 테스트하세요.
보장을 유지하는 데 비용이 커지거나 취약해질 징후를 감시하세요:
스파이크뿐 아니라 추세에 경고를 걸고, 지표를 해당 엔드포인트나 작업과 연동하세요.
불변식을 보호하는 데 필요한 가장 약한(즉, 비용이 적은) 격리 수준을 사용하세요. 기본값으로 무턱대고 최고 수준을 올리지 마세요. 작은 중요 구간(자금 이동, 재고 차감)에는 엄격한 설정을 적용하고, 나머지는 외부로 빼서 성능을 확보하세요.
ACID는 실패와 동시성 상황에서 데이터베이스가 예측 가능하게 동작하도록 돕는 트랜잭션 보장들의 집합입니다:
트랜잭션은 데이터베이스가 하나의 ‘패키지’로 다루는 단위 작업입니다. 여러 SQL 문을 실행하더라도 결과는 두 가지 중 하나로 끝납니다:
부분 업데이트는 현실 세계에서 모순을 만듭니다. 예를 들어:
ACID(특히 원자성과 일관성)는 이런 ‘반쪽짜리’ 상태가 진실로 보이지 않도록 막아줍니다.
원자성은 데이터베이스가 ‘반쯤 완료된’ 트랜잭션을 노출하지 않도록 보장합니다. 커밋 전에 앱이 크래시하거나 네트워크가 끊기거나 DB가 재시작되면, 트랜잭션은 롤백되어 이전 단계의 변경이 지속되지 않습니다.
실무에서는 두 계정의 잔액을 동시에 바꾸는 이체 같은 다단계 변경을 안전하게 만드는 것이 바로 원자성입니다.
클라이언트가 응답을 못 받는 경우(예: 네트워크 타임아웃 직후 커밋된 경우) 커밋이 실제로 되었는지 확신할 수 없습니다. 따라서 ACID와 함께 다음을 사용해야 합니다:
이 조합은 부분 업데이트와 중복 청구/중복 기록을 모두 방지합니다.
ACID의 ‘일관성’은 데이터가 ‘그럴듯해 보이는 것’이 아니라, 트랜잭션이 정의한 규칙들(제약, 트리거, 불변식)에 따라 한 유효한 상태에서 다른 유효한 상태로 이동해야 함을 뜻합니다.
예를 들어 “잔액은 음수가 되어선 안 된다”는 규칙을 어디에도 명시하지 않으면 ACID가 그 규칙을 강제할 수 없습니다. 데이터베이스는 명시된 불변식에 대해서만 일관성을 보장합니다.
앱 레벨 검증은 UX와 초기사전검사에 중요하지만 동시성 상황에서는 부족할 수 있습니다(예: 두 요청이 동시에 ‘이메일 사용 가능’ 체크를 통과). 데이터베이스 제약은 최종 방어선 역할을 합니다:
따라서 둘을 함께 사용하세요: 앱에서 미리 검증하고, DB에서 확실히 강제합니다.
격리성은 트랜잭션이 다른 트랜잭션을 ‘보거나’ 영향을 받는 방식을 제어합니다. 약한 격리에서는 다음과 같은 이상 현상이 발생할 수 있습니다:
격리 수준은 이러한 이상을 어느 정도 방지할지 선택하게 해 줍니다.
많은 OLTP 애플리케이션에서 기본값으로 Read Committed를 사용하는 것이 실용적입니다(더티 리드 방지, 우수한 성능). 필요하다면 위로 올리세요:
항상 사용하는 DB 엔진의 동작을 문서에서 확인하고, 실제로 문제되는 이상 현상을 테스트하세요.
내구성은 데이터베이스가 커밋을 확인한 이후 그 변경을 장애가 나도 잊지 않겠다는 약속입니다. 대부분의 관계형 DB는 **WAL(Write-Ahead Logging)**을 사용합니다: 변경 기록을 로그에 먼저 순차적으로 기록한 뒤 커밋을 확인합니다. 장애 시 로그를 재생해 커밋된 변경을 복구합니다.
구성에 따라 내구성 강도는 달라집니다:
백업과 복제는 복구/가용성에 도움을 주지만, 내구성과는 별개의 보장입니다.
트랜잭션을 BEGIN하고 COMMIT할 때 DB는 누가 어떤 행을 읽고 쓰는지, 충돌이 나면 어떻게 처리할지 조율합니다.
주요 기법:
잠금이 교착 상태(데드락)를 만들면 DB가 사이클을 감지해 한쪽 트랜잭션을 중단시키고(데드락 희생자) 애플리케이션이 재시도하도록 만드는 것이 일반적입니다.
ACID 보장은 API 설계, 백그라운드 작업, UI 흐름 등 여러 설계 결정을 좌우합니다. 핵심 아이디어는 어떤 단계들이 함께 성공해야 하는지를 정의하고, 그 단계들만 트랜잭션 안에 넣는 것입니다.
실무 권장 패턴:
/checkout에서 주문 생성·재고 확보·결제 의도 기록을 하나의 트랜잭션 범위로).에러 처리:
단일 DB 내에서는 ACID를 지키기 쉽지만, 서비스가 여러 개로 분산되면 같은 수준의 보장을 유지하기가 어렵고 비용이 커집니다.
분산 환경에서의 현실적 패턴:
중요 불변식(예: “계좌는 절대 초과 인출되지 않아야 함”)이 있다면 가능한 한 하나의 서비스와 하나의 DB 트랜잭션 안에 유지하세요.
트랜잭션은 유닛 테스트에서는 맞더라도 실제 트래픽과 재시작, 동시성에서는 깨질 수 있습니다. 실무 체크리스트:
규칙 요약: 불변식을 보호하는 데 필요한 최소한의 격리와 최소 범위의 트랜잭션을 사용하세요. 모든 것을 기본값으로 최고 수준으로 올리지 마세요—성능과 확장성에 큰 비용이 듭니다.