데이터베이스는 데이터를 저장하는 창고가 아니다

vx_developer·2026년 9월 21일

개발하다가

목록 보기
31/41
post-thumbnail

프로그래밍을 처음 배울 때 데이터베이스는 보통 데이터를 저장하고 필요할 때 다시 꺼내는 공간이라고 배운다.

await database.orders.insert({
  productId: "product_101",
  quantity: 2,
});

데이터를 저장하고 조회하는 기본 동작을 이해하기에는 충분한 설명이다.

하지만 실제 쇼핑몰 서비스를 개발하면 단순히 데이터를 넣고 꺼내는 것보다 더 많은 판단이 필요하다.

  • 주문할 당시의 상품 가격을 저장해야 하는가?
  • 상품 이름이 바뀌면 과거 주문 내역도 함께 바뀌어야 하는가?
  • 장바구니의 합계와 결제 완료 금액 중 무엇이 실제 거래 금액인가?
  • 주문을 취소하면 데이터를 삭제해야 하는가, 상태만 변경해야 하는가?
  • 동시에 들어온 두 주문이 마지막 재고를 구매하지 못하게 하려면 어떻게 해야 하는가?
  • 개인정보는 얼마나 오래 보관하고 언제 삭제해야 하는가?
  • 잘못된 값이 애플리케이션 코드를 우회해 저장되는 것을 어떻게 막아야 하는가?

데이터베이스는 단순히 값을 보관하는 창고가 아니다.

데이터베이스는 서비스가 기억해야 하는 사실을 정의하고, 그 사실의 관계·유효성·변경 이력을 일관되게 관리하는 시스템이다.


무엇을 저장할지 결정하는 순간 서비스가 기억할 사실이 정해진다

크리스가 쇼핑몰의 주문을 다음과 같이 저장한다고 생각해 보자.

type Order = {
  id: string;
  userId: string;
  productId: string;
  quantity: number;
};

이 구조는 누가 어떤 상품을 몇 개 주문했는지는 기억한다. 그러나 실제 주문을 다시 설명하기에는 정보가 부족하다.

상품 가격과 이름을 현재 상품 정보에서 가져오도록 구현할 수 있다.

async function getOrderDetail(orderId: string) {
  const order = await orderRepository.findById(
    orderId
  );

  const product =
    await productRepository.findById(
      order.productId
    );

  return {
    ...order,
    productName: product.name,
    unitPrice: product.price,
    totalPrice:
      product.price * order.quantity,
  };
}

이 코드는 현재 상품 정보를 이용해 주문 상세를 만든다.

문제는 상품 가격이 변경되었을 때 발생한다. 고객이 개당 20달러에 상품 두 개를 구매했더라도 현재 가격이 25달러라면 주문 내역에는 50달러가 표시된다.

주문 시점의 거래 사실과 현재 상품 정보가 섞였기 때문이다.

주문 항목은 거래가 일어났을 때의 정보를 별도로 기억해야 한다.

type OrderItem = {
  id: string;
  orderId: string;
  productId: string;
  productNameSnapshot: string;
  unitPriceSnapshot: number;
  quantity: number;
};

productId는 현재 상품과의 관계를 유지한다. productNameSnapshot과 unitPriceSnapshot은 주문이 발생한 시점의 사실을 보존한다.

같은 상품에서 출발한 값이라도 역할은 다르다.

  • 상품 테이블의 가격은 현재 판매 가격이다.
  • 주문 항목의 가격은 거래 당시 확정된 가격이다.
  • 장바구니의 합계는 아직 변경될 수 있는 예상 금액이다.
  • 결제 기록의 승인 금액은 결제 제공자가 처리한 거래 결과다.

데이터베이스 설계는 필드를 몇 개 만들 것인지 결정하는 작업에 그치지 않는다. 서비스가 미래에도 설명할 수 있어야 하는 사실이 무엇인지 정하는 작업이다.


현재 상태만 저장하면 과거에 일어난 일을 설명하지 못할 수 있다

다음 주문 구조는 현재 상태를 보여준다.

type Order = {
  id: string;
  status:
    | "pending"
    | "paid"
    | "shipped"
    | "cancelled";
  updatedAt: Date;
};

현재 주문이 배송되었는지 취소되었는지는 알 수 있다.

그러나 고객 지원 담당자가 다음 질문을 한다면 답하기 어렵다.

  • 결제는 언제 완료되었는가?
  • 배송 준비 상태에는 얼마나 오래 머물렀는가?
  • 취소 요청은 누가 처리했는가?
  • 배송 처리 후 다시 취소 상태로 바뀐 이유는 무엇인가?

현재 상태를 덮어쓰기만 하면 이전 상태는 사라진다.

변경 과정을 별도의 사건으로 저장할 수 있다.

type OrderStatusHistory = {
  id: string;
  orderId: string;
  fromStatus: OrderStatus | null;
  toStatus: OrderStatus;
  changedAt: Date;
  changedBy: string;
  reason?: string;
};

주문 테이블은 현재 상태를 빠르게 조회하는 데 사용한다. 상태 이력은 주문이 어떻게 현재 상태에 도달했는지 설명한다.

모든 데이터에 전체 이력이 필요한 것은 아니다. 임시 필터 값이나 화면 정렬 방식까지 데이터베이스에 기록할 필요는 없다.

반면 주문, 결제, 환불, 재고처럼 고객의 권리나 실제 거래에 영향을 주는 값은 변경 과정이 중요할 수 있다.

따라서 저장 여부를 결정할 때는 단순히 “나중에 사용할 것인가”만 물어서는 부족하다.

미래에 이 서비스가 어떤 사실을 증명하거나 설명해야 하는가?

이 질문이 데이터베이스에 남겨야 할 기록의 범위를 결정한다.


하나의 사실을 여러 곳에 저장하면 Source of Truth가 흐려진다

크리스가 주문 합계를 다음과 같이 저장했다고 생각해 보자.

type Order = {
  id: string;
  subtotal: number;
  discountAmount: number;
  shippingFee: number;
  totalAmount: number;
};

totalAmount는 다음 계산으로 만들 수 있다.

const totalAmount =
  subtotal - discountAmount + shippingFee;

계산 가능한 값을 저장하면 조회가 편해질 수 있다. 그러나 각 필드가 서로 다른 시점에 수정되면 모순이 생긴다.

await orderRepository.update(orderId, {
  discountAmount: 10,
});

할인 금액만 변경하고 totalAmount를 다시 계산하지 않으면 데이터베이스에는 서로 맞지 않는 값이 남는다.

그렇다고 계산된 값은 절대 저장하면 안 된다는 뜻은 아니다.

주문 금액은 다음과 같은 이유로 저장할 수 있다.

  • 결제 시점의 최종 금액을 보존해야 한다.
  • 복잡한 가격 정책을 매번 다시 실행하지 않아야 한다.
  • 과거 정책이 변경되어도 당시 거래 결과는 유지되어야 한다.
  • 결제 제공자의 승인 금액과 비교해야 한다.

중요한 것은 어떤 값이 원본이고 어떤 값이 계산 결과인지 명확히 정하는 것이다.

type OrderPricing = {
  itemSubtotal: number;
  discountAmount: number;
  shippingFee: number;
  chargedAmount: number;
  pricingPolicyVersion: string;
};

여기서 각 값의 의미를 다음과 같이 정의할 수 있다.

  • itemSubtotal은 주문 항목 가격의 합계다.
  • discountAmount는 주문에 적용되어 확정된 할인 금액이다.
  • shippingFee는 주문 시점에 확정된 배송비다.
  • chargedAmount는 실제 결제를 요청한 금액이다.
  • pricingPolicyVersion은 어떤 가격 정책으로 계산했는지 나타낸다.

Source of Truth는 반드시 하나의 테이블만을 의미하지 않는다. 사실의 종류마다 책임지는 원본이 다를 수 있다.

사실Source of Truth
현재 상품 판매 가격상품 데이터
주문 당시 상품 가격주문 항목 스냅숏
실제 승인된 결제 금액결제 기록 또는 결제 제공자
현재 주문 처리 상태주문 데이터
주문 상태 변경 과정주문 상태 이력
화면에 표시 중인 합계서버 데이터로 계산한 UI 상태

화면에 표시된 값이나 브라우저 상태는 사용자에게 보여주는 표현이다. 실제 거래 사실의 Source of Truth가 되지는 않는다.


데이터베이스 제약 조건은 마지막 방어선이 된다

주문 수량을 TypeScript 타입으로 선언할 수 있다.

type CreateOrderItemInput = {
  productId: string;
  quantity: number;
};

하지만 number는 수량이 양의 정수라는 사실까지 보장하지 않는다.

외부 입력은 검증 전까지 신뢰할 수 없다.

const createOrderItemSchema = z.object({
  productId: z.string().min(1),
  quantity: z.number().int().min(1).max(20),
});

서버 검증은 사용자에게 빠르고 구체적인 오류를 전달하는 데 필요하다.

그러나 애플리케이션 검증만으로 모든 저장 경로를 통제할 수 있다고 가정해서는 안 된다.

  • 다른 서버 코드에서 저장소를 직접 호출할 수 있다.
  • 운영 스크립트가 데이터를 수정할 수 있다.
  • 배치 작업이 오래된 검증 로직을 사용할 수 있다.
  • 동시에 실행된 요청이 애플리케이션 검사를 통과할 수 있다.
  • 새로운 서비스가 같은 데이터베이스를 사용할 수 있다.

데이터베이스에도 지켜야 할 규칙을 표현해야 한다.

CREATE TABLE order_items (
  id UUID PRIMARY KEY,
  order_id UUID NOT NULL
    REFERENCES orders(id),
  product_id UUID NOT NULL
    REFERENCES products(id),
  unit_price_cents INTEGER NOT NULL
    CHECK (unit_price_cents >= 0),
  quantity INTEGER NOT NULL
    CHECK (quantity BETWEEN 1 AND 20)
);

이 구조는 다음 규칙을 데이터베이스 수준에서도 보장한다.

  • 각 주문 항목에는 식별자가 있어야 한다.
  • 존재하는 주문과 상품을 참조해야 한다.
  • 가격은 음수가 될 수 없다.
  • 수량은 1개 이상 20개 이하여야 한다.

이메일 중복 가입을 막는 규칙도 애플리케이션의 조회 결과에만 의존해서는 부족하다.

const existingUser =
  await userRepository.findByEmail(email);

if (!existingUser) {
  await userRepository.create({ email });
}

두 요청이 동시에 실행되면 둘 다 사용자가 없다고 판단할 수 있다.

고유 제약 조건이 최종적으로 중복을 막아야 한다.

ALTER TABLE users
ADD CONSTRAINT users_email_unique
UNIQUE (email);

애플리케이션 검증과 데이터베이스 제약 조건은 서로 대체하는 관계가 아니다.

  • 애플리케이션은 사용자에게 의미 있는 피드백을 제공한다.
  • 데이터베이스는 어떤 저장 경로에서도 깨져서는 안 되는 규칙을 보장한다.

저장 위치는 값의 생명주기와 책임에 따라 달라진다

쇼핑몰에서 발생하는 모든 값을 데이터베이스에 저장할 필요는 없다.

크리스가 상품 목록 페이지에서 다음 값을 사용한다고 생각해 보자.

const selectedCategory = "laptop";
const sortOption = "price_asc";
const isFilterPanelOpen = true;

이 값들은 현재 화면을 표현하는 UI 상태다. 사용자가 페이지를 떠난 뒤에도 서비스가 반드시 기억해야 하는 거래 사실은 아니다.

반면 다음 주문 정보는 서버에서 지속적으로 관리해야 한다.

type Order = {
  id: string;
  userId: string;
  status: OrderStatus;
  chargedAmount: number;
  createdAt: Date;
};

저장 위치는 데이터라는 이유만으로 결정되지 않는다. 값이 얼마나 오래 존재해야 하며 누가 책임져야 하는지에 따라 결정된다.

값의 성격적절한 관리 위치
함수 실행 중에만 필요한 계산 결과지역 변수
현재 화면에만 영향을 주는 값UI State
공유 가능한 검색 조건URL Query Parameter
사용자의 임시 장바구니Browser Storage 또는 Server
로그인한 사용자의 장바구니Database
주문·결제와 같은 영구 거래 기록Database
환경별 데이터베이스 주소Environment Variable
운영자가 변경하는 배송 정책Database 또는 Configuration
반복되는 가격 계산 규칙Domain Logic
빠른 조회를 위해 복제한 값Cache 또는 Read Model

같은 장바구니라도 요구사항에 따라 위치가 달라진다.

비회원의 일시적인 장바구니라면 브라우저 저장소로 충분할 수 있다. 여러 기기에서 이어서 사용해야 한다면 서버와 데이터베이스가 필요하다.

특정 기술을 먼저 선택하기보다 값의 생명주기, 공유 범위, 복구 필요성, 보안 수준을 먼저 판단해야 한다.


삭제는 행을 없애는 작업이 아니라 기억의 범위를 결정하는 일이다

사용자가 주문을 취소했다고 해서 주문 행을 즉시 삭제하면 문제가 생길 수 있다.

await orderRepository.delete(orderId);

이 코드는 주문 기록 자체를 제거한다.

그러면 서비스는 다음 사실을 설명하지 못할 수 있다.

  • 결제가 실제로 발생했는가?
  • 환불이 처리되었는가?
  • 재고가 복구되었는가?
  • 고객이 언제 취소했는가?
  • 분쟁이 발생했을 때 어떤 거래였는가?

주문 취소는 데이터의 부재가 아니라 주문 상태의 변화로 표현하는 편이 적절할 수 있다.

await orderRepository.update(orderId, {
  status: "cancelled",
  cancelledAt: new Date(),
  cancellationReason:
    "customer_requested",
});

이제 주문이 존재했고 이후 취소되었다는 사실을 유지할 수 있다.

그렇다고 모든 데이터를 영원히 저장해야 하는 것은 아니다. 개인정보에는 별도의 보관 목적과 기간이 필요하다.

예를 들어 거래 기록은 유지하되 배송에 사용한 개인정보는 더 이상 필요하지 않을 때 비식별화할 수 있다.

await orderRepository.anonymizeCustomerData(
  orderId,
  {
    customerName: "Deleted Customer",
    phoneNumber: null,
    shippingAddress: null,
  }
);

삭제 설계에서는 서로 다른 요구를 구분해야 한다.

  • 사용자의 화면에서 보이지 않게 하는가?
  • 비즈니스 상태를 취소로 변경하는가?
  • 복구할 수 있도록 논리적으로 삭제하는가?
  • 개인정보만 익명화하는가?
  • 법적·운영상 보관 기간이 끝난 뒤 물리적으로 삭제하는가?

deletedAt 필드를 추가하는 것만으로 삭제 정책이 완성되지는 않는다. 데이터의 종류마다 왜 보관하고 언제 제거할 것인지가 정의되어야 한다.


데이터베이스는 여러 요청 사이에서도 규칙을 지켜야 한다

상품 재고가 하나 남아 있고 두 사용자가 동시에 주문한다고 생각해 보자.

두 요청이 먼저 재고를 조회한다.

const product =
  await productRepository.findById(productId);

if (product.stock > 0) {
  await productRepository.update(productId, {
    stock: product.stock - 1,
  });
}

두 요청 모두 stock이 1인 값을 읽을 수 있다. 그러면 둘 다 주문에 성공했다고 판단할 수 있다.

한 함수만 보면 조건문이 올바르게 보이지만, 여러 요청이 같은 데이터를 동시에 변경하면 규칙이 깨질 수 있다.

재고 감소는 데이터베이스에서 조건과 변경을 하나의 작업으로 처리할 수 있다.

UPDATE products
SET stock = stock - 1
WHERE id = $1
  AND stock > 0;

변경된 행의 수가 0이라면 이미 재고가 없는 것이다.

const updated =
  await productRepository.decreaseStock(
    productId
  );

if (!updated) {
  return {
    status: "out_of_stock",
  };
}

이 코드는 먼저 읽고 나중에 쓰는 두 단계를 분리하지 않는다. 데이터베이스가 현재 값이 유효할 때만 변경하도록 한다.

주문 생성과 재고 감소가 함께 성공해야 한다면 두 작업의 경계도 고려해야 한다.

await database.transaction(async (tx) => {
  const decreased =
    await tx.products.decreaseStock(
      productId,
      quantity
    );

  if (!decreased) {
    throw new OutOfStockError(productId);
  }

  await tx.orders.create({
    userId,
    productId,
    quantity,
  });
});

이 작업에서는 재고만 감소하고 주문이 사라지거나, 주문만 생성되고 재고가 그대로 남아서는 안 된다.

트랜잭션과 동시성 제어는 이후 별도로 다룰 개념이지만, 여기서 중요한 점은 데이터베이스가 단순한 저장 위치가 아니라 여러 요청 사이에서 사실의 일관성을 지키는 역할도 한다는 것이다.


데이터 구조는 현재 화면보다 서비스의 개념을 따라야 한다

주문 확인 화면이 다음과 같은 형태라고 생각해 보자.

type CheckoutScreenData = {
  productNames: string[];
  totalAmount: number;
  shippingAddressText: string;
  paymentMethodLabel: string;
};

이 구조를 그대로 하나의 데이터베이스 행에 저장하면 화면을 만들기는 편할 수 있다.

그러나 화면은 변경된다. 모바일 화면과 관리자 화면도 서로 다른 데이터를 요구한다.

데이터베이스 구조가 특정 화면에 지나치게 맞춰지면 다음 문제가 생긴다.

  • 상품별 수량이나 가격을 독립적으로 조회하기 어렵다.
  • 주문과 결제의 상태를 따로 관리하기 어렵다.
  • 주소 형식이 변경되면 기존 문자열을 해석해야 한다.
  • 환불 대상 주문 항목을 찾기 어렵다.
  • 새로운 화면을 만들 때 저장 구조를 다시 바꿔야 한다.

데이터베이스에는 화면 구성보다 서비스 개념을 표현하는 것이 우선이다.

type Order = {
  id: string;
  userId: string;
  status: OrderStatus;
  shippingAddressId: string;
  createdAt: Date;
};

type OrderItem = {
  id: string;
  orderId: string;
  productId: string;
  unitPriceSnapshot: number;
  quantity: number;
};

type Payment = {
  id: string;
  orderId: string;
  status: PaymentStatus;
  approvedAmount: number;
  providerTransactionId: string;
};

주문, 주문 항목, 결제는 서로 관계가 있지만 같은 개념은 아니다.

화면에 필요한 데이터는 이 사실들을 조합해 만들 수 있다.

async function getOrderSummary(
  orderId: string
) {
  const [order, items, payment] =
    await Promise.all([
      orderRepository.findById(orderId),
      orderItemRepository.findByOrderId(
        orderId
      ),
      paymentRepository.findByOrderId(
        orderId
      ),
    ]);

  return buildOrderSummary({
    order,
    items,
    payment,
  });
}

데이터베이스는 현재 화면의 모양을 복사하는 곳이 아니다. 여러 화면과 기능이 함께 사용할 서비스의 사실을 관리하는 곳이다.


주문 데이터는 서비스 전체를 이동하며 하나의 사실로 남는다

주문 정보는 한 번의 INSERT로 끝나지 않는다.

flowchart LR
    A[사용자 주문 입력] --> B[클라이언트 검증]
    B --> C[API 요청]
    C --> D[서버 검증]
    D --> E[상품과 재고 확인]
    E --> F[가격 정책 적용]
    F --> G[주문과 주문 항목 저장]
    G --> H[결제 요청]
    H --> I[결제 결과 기록]
    I --> J[주문 상태 변경]
    J --> K[주문 완료 화면]
    J --> L[배송 처리]
    J --> M[고객 지원 조회]

각 단계는 서로 다른 책임을 가진다.

  • 클라이언트 검증은 빠른 피드백을 제공한다.
  • 서버 검증은 신뢰할 수 없는 입력을 차단한다.
  • 가격 계산은 적용된 비즈니스 규칙을 결정한다.
  • 주문 데이터는 거래 시점의 정보를 보존한다.
  • 결제 데이터는 외부 결제 결과를 기록한다.
  • 주문 상태는 현재 처리 단계를 나타낸다.
  • 이력 데이터는 상태가 변한 과정을 설명한다.
  • 화면은 서버의 사실을 사용자에게 보여준다.

사용자가 브라우저를 닫더라도 주문 사실은 사라지지 않아야 한다. 다른 기기에서 접속하거나 고객 지원 담당자가 조회해도 같은 주문을 확인할 수 있어야 한다.

이것이 데이터베이스가 단순한 메모리나 파일과 다른 이유다. 서비스가 실행되는 한순간이 아니라 여러 요청과 시간에 걸쳐 유지해야 하는 사실을 책임진다.


데이터베이스를 설계하기 전에 물어봐야 할 질문

서비스가 기억해야 하는 사실인가

  1. 이 값은 일시적인 화면 상태인가, 서비스가 지속적으로 기억해야 하는 사실인가?
  2. 현재 값만 필요한가, 변경 과정도 기록해야 하는가?
  3. 미래에 고객이나 운영자에게 이 값을 설명하거나 증명해야 하는가?
  4. 사용자가 브라우저를 닫아도 유지되어야 하는가?
  5. 여러 사용자나 시스템이 같은 값을 공유해야 하는가?

Source of Truth가 분명한가

  1. 이 사실의 원본은 어느 시스템과 테이블이 관리하는가?
  2. 저장된 값은 원본인가, 스냅숏인가, 계산 결과인가?
  3. 같은 의미의 값을 여러 곳에 중복 저장하고 있지 않은가?
  4. 중복 저장한다면 서로 다를 때 어떤 값을 신뢰하는가?
  5. 화면 상태를 실제 비즈니스 상태로 오해하고 있지 않은가?

데이터의 유효성을 지킬 수 있는가

  1. 외부 입력을 서버에서 다시 검증하는가?
  2. NOT NULL, UNIQUE, CHECK, 외래 키로 보장할 규칙이 있는가?
  3. 애플리케이션 코드를 우회한 저장도 안전한가?
  4. 동시에 실행되는 요청이 같은 규칙을 깨뜨릴 수 있는가?
  5. 관련된 변경이 함께 성공하거나 실패해야 하는가?

생명주기와 삭제 정책이 정의되어 있는가

  1. 데이터는 언제 생성되고 언제 더 이상 필요하지 않은가?
  2. 취소와 삭제를 같은 의미로 처리하고 있지 않은가?
  3. 거래 기록과 개인정보의 보관 기간이 다른가?
  4. 논리적 삭제, 익명화, 물리적 삭제 중 무엇이 필요한가?
  5. 삭제된 데이터가 백업, 로그, 캐시에도 남는지 고려했는가?

운영 중 변화에 대응할 수 있는가

  1. 새로운 필드를 추가할 때 기존 데이터는 어떻게 처리하는가?
  2. 가격 정책이 바뀌어도 과거 주문을 재현할 수 있는가?
  3. 잘못된 데이터가 저장되었을 때 영향을 추적할 수 있는가?
  4. 데이터 마이그레이션을 중단하거나 되돌릴 방법이 있는가?
  5. 백업이 존재하는 것뿐 아니라 실제 복구가 가능한지 확인했는가?

흔한 실수는 데이터의 의미를 잃게 만든다

화면에 필요한 값을 그대로 한 행에 저장한다

await database.orders.insert({
  screenText:
    "Laptop × 2 / Express / $2,050",
});

현재 화면은 쉽게 만들 수 있지만 상품, 수량, 배송 방식, 금액을 독립적으로 검증하거나 변경하기 어렵다.

화면 문자열이 아니라 그 화면을 만드는 서비스의 사실을 저장해야 한다.


현재 상품 정보로 과거 주문을 다시 계산한다

const total =
  currentProduct.price * order.quantity;

상품 가격이 변경되면 과거 거래 금액도 바뀐다.

주문 시점에 확정된 가격은 주문 항목의 스냅숏으로 보존해야 한다.


애플리케이션 검증만 믿는다

if (quantity > 0) {
  await saveOrderItem({ quantity });
}

다른 저장 경로나 동시 요청이 규칙을 우회할 수 있다.

서비스에서 절대 깨져서는 안 되는 규칙은 데이터베이스 제약 조건으로도 보호해야 한다.


취소된 데이터를 즉시 삭제한다

await orderRepository.delete(orderId);

거래와 환불의 근거까지 사라질 수 있다.

취소, 논리적 삭제, 익명화, 물리적 삭제를 서로 다른 작업으로 설계해야 한다.


계산된 값을 여러 곳에서 각자 수정한다

await orderRepository.update(orderId, {
  totalAmount: newTotal,
});

await paymentRepository.update(paymentId, {
  amount: anotherTotal,
});

두 값이 달라졌을 때 어느 쪽이 실제 거래 금액인지 판단하기 어렵다.

각 사실의 Source of Truth와 동기화 책임을 명시해야 한다.


백업이 있다는 이유로 복구할 수 있다고 가정한다

백업 파일이 존재해도 복구 절차, 소요 시간, 누락 범위를 확인하지 않았다면 실제 장애에서 사용할 수 있다고 보장할 수 없다.

데이터베이스가 서비스의 기억이라면 백업은 그 기억을 다시 복원할 수 있는 검증된 수단이어야 한다.


데이터베이스의 핵심은 서비스가 기억할 사실을 책임지는 데 있다

프로그래밍을 처음 배울 때는 다음 코드로 데이터베이스의 기본 동작을 이해할 수 있다.

await database.orders.insert(order);

const savedOrder =
  await database.orders.findById(order.id);

데이터를 저장하고 다시 조회할 수 있다는 설명은 입문 단계에서 충분하다.

하지만 실제 서비스에서는 저장 명령보다 저장할 사실의 의미를 먼저 판단해야 한다.

  • 현재 정보와 거래 당시 정보를 구분했는가?
  • 현재 상태뿐 아니라 필요한 변경 과정도 기록하는가?
  • 원본, 스냅숏, 계산 결과의 책임이 분명한가?
  • 외부 입력이 애플리케이션과 데이터베이스 양쪽에서 보호되는가?
  • 동시에 실행되는 요청도 비즈니스 규칙을 지킬 수 있는가?
  • 일시적인 UI 상태와 영구적인 서비스 사실을 구분했는가?
  • 취소, 논리적 삭제, 익명화, 물리적 삭제의 의미가 분리되어 있는가?
  • 현재 화면이 바뀌어도 데이터가 서비스의 개념을 설명할 수 있는가?
  • 운영 중 과거 거래를 추적하고 복구할 수 있는가?
  • 사실마다 신뢰해야 할 Source of Truth가 정해져 있는가?

데이터베이스는 데이터를 저장하는 창고가 아니다.

서비스가 기억해야 하는 사실을 정의하고, 그 사실의 관계·유효성·변경 이력을 여러 요청과 시간에 걸쳐 일관되게 관리하는 시스템이다.

profile
Vision eXperience Developer

0개의 댓글