2차 프로젝트 테이블 설계 - DELIVERYS (배송 정보) 테이블
배송 처리 "이 주문들 오늘 출고해야지." 특정 order_id에 해당하는 레코드에 carrier와 tracking_number를 **입력(INSERT/UPDATE)**하고 status를 SHIPPED로 변경
문제 대응 "배송 중 누락된 건이 있네?" status가 장기간 IN_TRANSIT인 레코드를 조회하여 문제 파악
고객 문의 대응 "고객이 배송지 변경 요청했네." 특정 id 레코드의 address, phone_number 필드를 수정(UPDATE)
일괄 처리 "오늘 발송된 모든 주문 리스트" shipped_at 날짜 기준으로 전체 조회 및 엑셀 다운로드
관리자의 관심사: 모든 필드(특히 주소, 연락처, 운송장 번호 입력/수정), 모든 레코드의 상태 변경 이력.
요약
사용자: 자신의 짐(order_id)이 어디 있는지 확인(status, tracking_number)만 할 수 있습니다.
관리자: 모든 짐을 관리(INSERT, UPDATE, 전체 조회)하고 상태를 변경합니다.
앞서 설계한 deliveries 테이블은 이미 외부 API 연동을 염두에 두고 설계되었습니다.
address, address_detail, zip_code 필드는 카카오맵 주소 API를 통해 받은 표준화된 정보를 저장하기에 적합합니다.
tracking_number와 carrier, 그리고 status 필드는 우체국이나 타 택배사 API와 연동하여 자동으로 업데이트될 정보를 저장합니다
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '배송 고유 ID',
`order_id` VARCHAR(64) NOT NULL COMMENT '상점 주문 ID (orders.id 참조)',
`tracking_number` VARCHAR(100) NULL COMMENT '운송장 번호 (택배사 API 연동 정보)',
`carrier` VARCHAR(50) NULL COMMENT '택배사 (예: CJ대한통운, 우체국) (택배사 API 연동 정보)',
`status` ENUM('PENDING', 'SHIPPED', 'IN_TRANSIT', 'DELIVERED', 'EXCEPTION') NOT NULL DEFAULT 'PENDING' COMMENT '배송 상태 (택배사 API 연동 정보)',
`recipient_name` VARCHAR(100) NOT NULL COMMENT '수령인 이름',
`address` VARCHAR(255) NOT NULL COMMENT '배송지 주소 (카카오맵 API로 검증)',
`address_detail` VARCHAR(255) NULL COMMENT '상세 주소',
`zip_code` VARCHAR(10) NOT NULL COMMENT '우편번호 (카카오맵 API로 검증)',
`phone_number` VARCHAR(20) NOT NULL COMMENT '수령인 연락처',
`shipped_at` DATETIME NULL COMMENT '발송일시',
`delivered_at` DATETIME NULL COMMENT '배송 완료일시',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_order_id` (`order_id`),
INDEX `idx_tracking_number` (`tracking_number`),
FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='배송 정보 테이블';
딜리버리 테이블에 XML 원본 데이터를 저장하는 컬럼을 두는 것은 좋은 접근
배송 테이블에 XML 원본 데이터를 저장하는 컬럼을 두는 것은 좋은 접근 방식입니다. 이는 시스템의 견고함과 유지보수성을 크게 향상시킵니다.
- 필드 누락 대비: 초기 설계 시 필요한 필드만 파싱했지만, 나중에 다른 정보(예: 배송 기사 연락처, 상세 배송 지연 사유)가 필요해질 수 있습니다. 원본 데이터가 있으면 DB 스키마를 수정하지 않고도 나중에 해당 정보를 다시 파싱하여 활용할 수 있습니다.
- 실제 API 호출 없이 저장된 XML 데이터를 사용하여 파싱 로직을 테스트하고 재현할 수 있습니다.
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '배송 고유 ID',
`order_id` VARCHAR(64) NOT NULL COMMENT '상점 주문 ID (orders.id 참조)',
`tracking_number` VARCHAR(100) NULL COMMENT '운송장 번호',
`carrier` VARCHAR(50) NULL COMMENT '택배사',
`status` ENUM('PENDING', 'SHIPPED', 'IN_TRANSIT', 'DELIVERED', 'EXCEPTION') NOT NULL DEFAULT 'PENDING' COMMENT '배송 상태',
-- ... (주소 및 연락처 필드 생략) ...
`shipped_at` DATETIME NULL COMMENT '발송일시',
`delivered_at` DATETIME NULL COMMENT '배송 완료일시',
**`raw_api_response` TEXT NULL COMMENT '택배사 API 원본 XML/JSON 응답 데이터',**
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시',
PRIMARY KEY (`id`),
-- ... (기타 인덱스 및 FK 생략) ...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='배송 정보 테이블';
TEXT 타입은 길이가 긴 XML 데이터를 저장하기에 적합합니다.
만약 API 응답이 JSON 형식이라면, MySQL 5.7+ 버전에서는 JSON 타입을 사용하는 것이 더 효율적일 수 있습니다.
포트폴리오 부각 포인트
이 raw_api_response 컬럼을 사용하면 면접 시 **"데이터의 무결성을 위해 외부 API 통신 시 원본 데이터를 백업하여, 추후 유지보수 및 디버깅 용이성을 확보했습니다"**라고 설명하며 실무적인 고민을 했다는 점을 어필할 수 있습니다.
딜리버리 테이블 개선 2
배송비 부분은 어케하면 좋겠음?
배송비 부분은 시스템의 여러 테이블과 로직에 걸쳐 영향을 미치는 중요한 요소입니다. 배송비 정책을 구현하기 위해 다음 영역들을 고려해야 합니다.
CREATE TABLE `delivery_policies` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '정책 고유 ID',
`policy_name` VARCHAR(100) NOT NULL COMMENT '정책 이름 (예: 기본 정책, 제주도 정책)',
`base_fee` DECIMAL(10, 2) NOT NULL COMMENT '기본 배송비',
`free_shipping_threshold` DECIMAL(10, 2) NULL COMMENT '무료 배송 기준 금액 (NULL이면 기준 없음)',
`is_active` BOOLEAN NOT NULL DEFAULT TRUE COMMENT '활성화 여부',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='배송비 정책 관리 테이블';
ALTER TABLE `orders`
ADD COLUMN `shipping_fee` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '주문 시 부과된 배송비',
ADD COLUMN `applied_delivery_policy_id` INT UNSIGNED NULL COMMENT '적용된 배송 정책 ID (delivery_policies.id 참조)';
deliveries 테이블 하나로 배송비 관련 모든 정보를 처리하는 것도 가능은 합니다.
가장 좋은 방법은 배송 정책을 위한 별도 테이블을 유지하고, 주문 시점에 확정된 배송비 금액만 orders 또는 deliveries 테이블에 저장하는 것입니다.
orders 테이블에 배송비 금액 저장 (권장)
orders 테이블은 주문 시 확정된 금액(상품 총액, 할인액, 배송비)을 모두 가지고 있어야 최종 결제 금액이 명확해집니다.
orders 테이블: shipping_fee (금액), total_amount (최종 결제 금액 포함)
deliveries 테이블: additional_fee (추가 비용 금액), carrier, tracking_number 등 배송 실물 정보
deliveries 테이블 하나로 기본 배송비 정책(5만원 미만 유료, 5만원 이상 무료)과 도서 산간 지역 추가 비용까지 처리하는 스키마입니다.
이 설계는 앞서 설명한 정규화 원칙에 일부 위배되지만, 테이블 수를 최소화하면서 필요한 정보를 저장할 수 있도록 주문 시 확정된 금액을 저장하는 방식입니다.`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '배송 고유 ID',
`order_id` VARCHAR(64) NOT NULL COMMENT '상점 주문 ID (orders.id 참조)',
-- 배송비 관련 필드 --
`product_total_amount` DECIMAL(10, 2) NOT NULL COMMENT '상품 총 금액 (배송비 계산 기준)',
`base_shipping_fee` DECIMAL(10, 2) NOT NULL COMMENT '기본 배송비 (주문 시 정책 기준)',
`additional_fee` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '도서산간 추가 비용',
`final_shipping_fee` DECIMAL(10, 2) NOT NULL COMMENT '최종 청구된 배송비 (base + additional)',
-- 배송 추적 관련 필드 --
`tracking_number` VARCHAR(100) NULL COMMENT '운송장 번호',
`carrier` VARCHAR(50) NULL COMMENT '택배사',
`status` ENUM('PENDING', 'SHIPPED', 'IN_TRANSIT', 'DELIVERED', 'EXCEPTION') NOT NULL DEFAULT 'PENDING' COMMENT '배송 상태',
-- 주소 정보 관련 필드 --
`recipient_name` VARCHAR(100) NOT NULL COMMENT '수령인 이름',
`address` VARCHAR(255) NOT NULL COMMENT '배송지 주소',
`address_detail` VARCHAR(255) NULL COMMENT '상세 주소',
`zip_code` VARCHAR(10) NOT NULL COMMENT '우편번호',
`phone_number` VARCHAR(20) NOT NULL COMMENT '수령인 연락처',
-- 시간 및 기타 정보 --
`shipped_at` DATETIME NULL COMMENT '발송일시',
`delivered_at` DATETIME NULL COMMENT '배송 완료일시',
`raw_api_response` TEXT NULL COMMENT '택배사 API 원본 XML/JSON 응답 데이터',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_order_id` (`order_id`),
INDEX `idx_tracking_number` (`tracking_number`),
FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='통합 배송 정보 테이블';
구조 좋습니다. 실무 기준으로도 꽤 잘 설계되어 있고, **배송 단일 구조(주문 1건 = 배송 1건)**을 전제로 설계했다는 게 보여요.
하지만 운영 환경을 고려하면 몇 가지를 꼭 생각해야 합니다:
✅ 먼저 좋은 점
| 요소 | 평가 |
|---|---|
| 배송비 계산 필드 분리 (base, additional, 최종) | 👍 명확하고 회계처리 유리 |
| 배송 상태 ENUM 단순화 | 👍 MVP 기준 적절 |
| API 원본 저장 컬럼 존재(raw_api_response) | 👍 장애 추적 및 택배사 API 이슈 디버깅에 매우 유용 |
| order_id UNIQUE → 주문 하나당 하나의 배송 | 👍 단순 쇼핑몰 구조에서는 문제없음 |
⚠️ 하지만 아래 경우가 생기면 구조가 깨집니다
| 시나리오 | 현재 스키마에서 문제 발생 |
|---|---|
| 부분 배송 (재고 부족 → 일부만 먼저 출고) | ❌ order_id UNIQUE 때문에 2개의 배송 생성 불가 |
| 다중 판매자(Multi Vendor, 마켓플레이스, 스마트스토어 방식) | ❌ 배송지가 동일해도 배송 2건 |
| 예약 / 당일출고 혼합 | ❌ 다른 배송타입 구분 필요 |
| 묶음배송 / 분할배송 정책 적용 | ❌ 불가능 |
현재 구조는:
orders (1) — (1) deliveries
하지만 실제 커머스 기준 표준은:
orders (1) — (N) delivery_groups (묶음 단위)
delivery_groups (1) — (N) deliveries (송장 단위)
📦 왜 delivery_group 이 필요한가?
예시:
| 주문 | 상품 | 배송구조 |
|---|---|---|
| A | 사과(농가1), 배(농가2) | 배송 2건 |
| B | 모니터 + 마우스 | 묶음배송 1건 |
| C | 옷 3개 중 1개 품절 → 나중 배송 | 분할배송 2건 |
🔧 추천 개선안 (확장형)
① delivery_groups (묶음배송 단위)
CREATE TABLE delivery_groups (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
group_status ENUM('PENDING','PARTIAL_SHIPPED','SHIPPED','DELIVERED','CLOSED') DEFAULT 'PENDING',
shipping_policy TEXT NULL COMMENT '묶음배송 규칙(판매자별, 옵션별 등)',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_order(order_id),
FOREIGN KEY (order_id) REFERENCES orders(id)
);
② deliveries는 송장 단위로 분리
지금 만든 deliveries 테이블을 유지하되:
order_idFK는delivery_group_id로 변경하는 게 더 자연스럽습니다.
🔁 수정 버전:
ALTER TABLE deliveries
CHANGE COLUMN order_id delivery_group_id BIGINT UNSIGNED NOT NULL;
ALTER TABLE deliveries
ADD FOREIGN KEY (delivery_group_id) REFERENCES delivery_groups(id);
🧭 배송 상태 플로우 표준화 제안
현재 ENUM은 택배사 기준인데, 주문 시스템 기준으로는 아래 구조가 더 적합합니다:
READY_FOR_PICKUP
→ SHIPPED
→ IN_TRANSIT
→ OUT_FOR_DELIVERY
→ DELIVERED
→ RETURNED / LOST / FAILED
추가하면 좋을 ENUM:
| 상태 | 이유 |
|---|---|
CANCELLED | 출고 직전 취소 |
RETURNED | 반품 회수 |
LOST | 운송사 분실 |
📍 결론
| 항목 | 현재 설계 상태 | 개선 필요 여부 |
|---|---|---|
| 배송비 계산 구조 | 🔥 완벽 | 유지 |
| 배송상태 ENUM | 👍 MVP는 OK, 확장되면 부족 | 개선 추천 |
| order_id UNIQUE | ❌ 부분배송 불가 | 구조 변경 권장 |
| 장기 확장성(마켓, 스마트스토어형) | ❌ 부족 | delivery_group 추가 설계 필요 |
다음 단계 제안
원하면 아래 중 하나 이어서 설계해 줄게:
부분배송과 반품 연결하는
return_items+delivery_items테이블라스트마일 택배 API 표준화 설계 (CJ/쿠팡/롯데/우체국 호환)
배송비 정책(무료, 우체국, 제주/도서 추가요금, 부피무게) 계산 엔진 설계
SmartStore형 수십 판매자 대응 Multi-Shipping Architecture
당신 질문 핵심은 이거임 👇
배송비가 deliveries 테이블에 있는데, 결제는 orders.total_amount 기준이니까… 배송비는 어디에 속해야 맞는가?
배송비가 주문금액에 포함될까? 배송 테이블 금액이 진짜 금액인가?
이거 제대로 이해하려면 "배송비는 계산 속성이지 결제 대상 금액 그 자체가 아니다" 라는 개념을 먼저 잡아야 해요.
🚀 정답
배송비는 결제 금액에 포함되며, deliveries 테이블에는 "실제 배송비 정책·계산 결과의 스냅샷"만 저장하는 것이 정답입니다.
즉:
-
결제 → orders.total_amount 에 포함
-
배송 데이터 → deliveries.final_shipping_fee 는 “참고용·기록용”
📦 왜 이렇게 나누는가?
배송비는 "결제 대상 금액"이기 때문에 결제 전에 이미 계산이 완료되어 있어야 함.
따라서 구조 흐름은 이렇게 되어야 함:
카트 → 배송 가능지역 체크 → 배송비 계산 → 주문 생성 → 결제 → 배송 생성
즉 배송비 테이블이 결제 금액을 "만드는 기준"이지
배송 테이블이 payment의 실결제금을 들고 있는 건 아님.
📌 역할 정리
| 위치 | 배송비 역할 | 결제 금액 포함 여부 |
|---|---|---|
orders.total_amount |
최종 결제 금액 (상품 + 배송비 - 할인) | ✅ 포함 |
deliveries.final_shipping_fee |
배송비 정책의 결과 스냅샷 (계산 기준 기록) | ❌ 결제 금액 소유 아님 |
🧠 예시
상품: 30,000
배송비: 3,000
도서산간 추가요금: 2,000
총 결제: 35,000원
| 테이블 | 값 |
|---|---|
| orders.total_amount | 35,000 |
| deliveries.final_shipping_fee | 5,000 (3,000 + 2,000) |
🔍 왜 deliveries에 배송비를 두냐면?
목적은 이것들 때문👇
| 이유 | 설명 |
|---|---|
| 📜 회계 추적 | 결제 당시 배송비 정책 기록 (무료 이벤트였는지 등) |
| 🚚 반품/환불 계산 | 부분 환불 때 배송비 환불 여부 판단 |
| 🧾 법적 감사/로그 | PG 정산 오류 대비 |
| 🧠 정책 적용 | 묶음 배송 / 판매자별 배송비 / 지역별 정책 확인 |
즉, deliveries.final_shipping_fee 는
"배송비를 계산한 증거기록" 이고
"결제 처리의 단일 소스"는 orders입니다.
📍 반품/환불에서의 중요한 포인트
환불할 때 배송비를 환불할지 여부는 deliveries.final_shipping_fee 값을 기반으로 로직이 작동하고,
refunds 테이블에서 다음과 같이 기록됨:
| 케이스 | 배송비 환불 여부 |
|---|---|
| 고객 변심 반품 | ❌ 배송비 보통 미환불 |
| 판매자 문제(불량/오배송) | ✅ 배송비 환불 |
| 부분환불 | ⚠ 조건 따라 다름 |
그래서 배송비는 정산/환불 로직의 input 데이터이지 결제 데이터의 owner가 아님.
🧱 최종 구조 요약
orders.total_amount = subtotal - discounts + shipping_fee
▲
│ (결제 대상)
│
deliveries.final_shipping_fee (결제 기준이 되는 배송비 스냅샷)
🏁 결론
✔ 배송비는 주문에 포함되어 결제된다.
✔ 배송 테이블은 배송비 계산 결과 스냅샷을 저장하는 곳이다.
✔ 결제 금액의 소유자는 orders, 배송비 정책 정보는 deliveries.
다음 선택지👇
원하면 이어서:
-
배송비 정책 계산 알고리즘 설계 (무료배송, 제주/도서, 합배송, 판매자별 정책)
-
부분 취소/반품 시 배송비 환불/차감 규칙 설계
-
PG/정산 기준 배송비 accounting model
번호 골라줘 😎
댓글
댓글 쓰기