.
릴레이션 스키마 - PK FK
- 사용자(users)
- 사용자 아이디(user_id, bigint)
- 이름(name, varchar(255))
- 이메일(email, varchar(255))
- 삭제 여부(deleted, tinyint, default = false)
- 사용자 포인트 잔액(user_points) - 사용자 아이디와 비식별 관계(포인트는 부채 항목이므로 탈퇴한 회원이더라도 회계 처리에 필요한 데이터), 사용자 필드의 다른 데이터와 업데이트 주기가 달라 별도의 도메인으로 분리. 대기업 입사 시험문제라는 점, 다중 서버를 사용하는 대용량 트래픽 환경이라는 점에서 업무가 분리되어 있을 것이라 판단함
- 포인트 id(points_id, bigint)
- 사용자 아이디(user_id, bigint, unique)
- 포인트 잔액(point_balance, bigint) <-- 조회 성능을 위한 cache column: 원본이 바뀌었을 때 동기화돼야 함
- 업데이트 날짜(updated_at, timestamp)
- 포인트 충전 이력(point_charges)
- 충전이력 아이디(charge_id, bigint)
- 회원 아이디(user_id, bigint)
- 충전 금액(charge_amount, bigint)
- 생성 시각(created_at, timestamp)
- 충전 유형(type, tinyint, 0 - 충전, 1 - 환불, 2 - 프로모션)
- 포인트 사용(결제) 이력(point_payments) - 불필요한 복잡성 피하기 위해 전체 취소만 허용
- 사용이력 아이디(point_payment_id, bigint)
- 회원 아이디(user_id, bigint)
- 주문 내역(order_id, bigint, UNIQUE) - 하나의 주문에 결제가 복수로 일어나면 안되므로
- 결제액(payment_amount, bigint) -> 실제로 차감된 금액. 감사 추적, 정산 및 회계 처리, 주문 삭제/변경 시 데이터 보호 위해 스냅샷 컬럼으로 추가
- 생성 시각(created_at, timestamp)
- 메뉴 (menus)
- 메뉴 아이디(menu_id, bigint)
- 이름(name, varchar(255))
- 가격(price, bigint)
- 삭제 여부(deleted, tinyint, default = false)
- 주문 내역 (orders)
- 주문내역 아이디(order_id, bigint)
- 주문자(user_id, bigint)
- 총 가격 (total_price, bigint)
- 주문 상태(order_status, tinyint, 0 - 결제 대기, 1 - 결제 완료, 2 - 결제 취소)
- 주문 생성 시각(created_at, timestamp)
- 주문 상품 (order_menus) - 주문에 논리적으로 완전히 의존되나 구현은 비식별 (독립 surrogate PK 사용)
1) 다중 서버 환경에서의 전역 식별자 전략과 충돌: 다중 서버 환경에서는 PK를 Snowflake ID나 UUID 같은 전역 단일 식별자로 관리하는 경우가 많은데, 복합 PK는 이 전략과 충돌
2) ORM 구현 단순성: JPA + 대용량 환경에서 연관 조회, 캐싱, 배치 처리 모두 복잡도가 올라감. order_id에 NOT NULL + Cascade Delete를 걸면 동일한 보장이 됨.
- 주문상품 아이디 (order_menu_id, bigint)
- 주문(order_id, bigint)
- 메뉴(menu_id, bigint)
- 주문상품명(name, varchar(255)) <-- snapshot: 주문 시점 이후 변경되더라도 동기화되면 안 됨
- 개당 가격(unit_price, bigint) <-- snapshot
- 수량(quantity, int)
시나리오
1. 주문 취소 시나리오 (하나의 트랜잭션으로 묶어 원자성 보장)
- orders.order_status == 1(완료) 여부 확인 — 이미 취소된 주문은 거부
- orders.order_status = 2(취소)로 업데이트
- point_payments에서 해당 order_id의 payment_amount 조회
- point_charges에 type=1(환불), charge_amount = payment_amount row INSERT
- user_points.point_balance += payment_amount
- user_points.updated_at 갱신
- 2~6을 하나의 트랜잭션으로 묶음
payment_amount를 스냅샷으로 저장해뒀기 때문에, 이후 total_price가 변경되더라도 실제 결제된 금액 기준으로 정확히 환불할 수 있음
2. 포인트 충전 시나리오
- point_charges에 type=0(충전) row INSERT
- user_points.point_balance += charge_amount 및 user_points.updated_at 갱신 (하나의 쿼리문으로 묶어 레코드 락이 걸리도록)
- 1~3을 하나의 트랜잭션으로 묶어 원자성 보장
충전 금액이 DB에 반영됐는데 캐시 컬럼 업데이트가 실패하면 잔액 불일치가 발생하는데, 이를 트랜잭션으로 묶어 둘 중 하나라도 실패하면 전체 롤백되도록 보장
3. 주문 생성 + 결제 시나리오
- orders row INSERT (order_status=0, 결제 대기)
- order_products에 상품별 row INSERT (name, unit_price 스냅샷 저장)
- user_points.point_balance >= total_price 잔액 검증
- 잔액 부족이면 롤백 후 예외 반환
- 잔액 충분하면 user_points.point_balance -= total_price
- point_payments row INSERT (payment_amount 스냅샷 저장)
- orders.order_status = 1(결제 완료)로 업데이트
- 1~7을 하나의 트랜잭션으로 묶음
3번 잔액 검증을 트랜잭션 바깥에서 하면 동시 요청 시 음수 잔액이 발생할 수 있음. SELECT FOR UPDATE로 user_points 행을 선점한 뒤 검증과 차감을 이어서 처리
4. 잔액 부족 시 결제 실패
- orders row INSERT (order_status=0, 결제 대기)
- order_products row INSERT
- user_points.point_balance < total_price → 잔액 부족 감지
- 트랜잭션 전체 롤백 (orders, order_products 포함)
- 클라이언트에 잔액 부족 에러 반환
주문 row를 먼저 INSERT했다가 롤백하는 대신, 잔액 검증을 먼저 하고 통과한 경우에만 주문을 생성하는 순서로 바꾸면 불필요한 INSERT를 줄일 수 있습니다.
5. 회원 탈퇴
- users.deleted = 1로 업데이트 (soft delete)
- user_points는 삭제하지 않음 — 부채 항목이므로 회계 처리 목적으로 보존
- point_charges, point_payments 이력도 보존
- 탈퇴 회원의 로그인 시도 시 deleted = 1 확인 후 거부
user_points를 users와 비식별 관계로 분리한 이유가 여기서 드러납니다. 탈퇴 회원의 users row가 soft delete되더라도 user_points는 독립적으로 존재하며, 회계 감사 시 포인트 부채 잔액을 그대로 추적할 수 있습니다.
6. 메뉴 삭제 시나리오
- menus.deleted = 1로 업데이트 (soft delete)
- order_products의 기존 데이터는 영향 없음 — name, unit_price 스냅샷이 이미 저장돼 있으므로
- 주문 가능 메뉴 조회 시 WHERE deleted = 0 필터 적용
면접 포인트
hard delete를 쓰면 order_products.menu_id FK 제약 위반이 발생. soft delete로 참조 무결성을 유지하면서, 스냅샷 컬럼 덕분에 삭제된 메뉴가 포함된 과거 주문도 정확하게 조회할 수 있음
시나리오별 트랜잭션 범위 요약
| 시나리오 | 트랜잭션 범위 |
| 포인트 충전 | point_charges INSERT + user_points UPDATE |
| 주문 생성 + 결제 | orders INSERT + order_products INSERT + user_points UPDATE + point_payments INSERT + orders UPDATE |
| 주문 취소 + 환불 | orders UPDATE + point_charges INSERT + user_points UPDATE |
| 잔액 부족 실패 | 전체 롤백 |
| 회원 탈퇴 | users UPDATE (나머지는 보존) |
| 메뉴 삭제 | menus UPDATE |
시나리오 시각화 플로우차트
.
'Projects > [Spring] Coffee Shop Project' 카테고리의 다른 글
| 트러블슈팅 : MySQL Access Denied 오류 해결하기 (0) | 2026.04.02 |
|---|---|
| 트러블슈팅: redisson-spring-boot-starter Spring Boot 4.x 호환성 오류 (0) | 2026.04.02 |
| API 명세서 만들어보기 (3) | 2026.04.01 |
| [데이터베이스] 2. 식별 관계와 비식별 관계 (8) | 2026.03.27 |
| [데이터베이스] 1. 데이터 모델링과 ERD 설계 (0) | 2026.03.26 |