-
Notifications
You must be signed in to change notification settings - Fork 2
Open
Description
프로젝트 개요
- hee-commerce는 티몬 과 같은 이커머스 서비스에서 운영될 수 있는 타임딜 서버로, 10분동안 TPS 3000를 감당할 수 있는 서버입니다.
- 문제 해결점을 찾기에 앞서 문제 정의부터 명확히하는 것이 중요하다고 생각하여, 서버 시나리오를 구글 Docs에 상황을 구체화한 후 프로젝트를 시작하였고, 특히 “주문 API”에 집중하여 프로젝트를 진행했습니다.
주문 API WorkFlow (수정 필요)
어떤 과정을 거쳐 v3까지 나왔는지 궁금하시면 wiki를 참고해주세요!
예외처리는 어디까지 생각했는지 궁금하시면 draw.io를 참고해주세요!
기술 스택
- JAVA 17, SpringBoot 3, MyBatis 3.5, MySQL 8, Redis 7.0, Flyway 9.5.1, RestDocs, Docker
DB 스키마 : uuid 오버엔지니어링 아닌지
CREATE TABLE deal (
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
type VARCHAR(255) NOT NULL,
started_at TIMESTAMP NOT NULL,
finished_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_at TIMESTAMP NULL,
deleted_at TIMESTAMP NULL
);
CREATE TABLE deal_product (
uuid BINARY(1) NOT NULL PRIMARY KEY,
deal_id INT NOT NULL,
product_uuid BINARY(1) NOT NULL,
deal_product_title VARCHAR(255) NOT NULL,
inventory INT NOT NULL,
discount_type VARCHAR(255) NOT NULL,
discount_value INT NOT NULL,
max_deal_order_quantity_per_order INT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_at TIMESTAMP NULL,
deleted_at TIMESTAMP NULL,
FOREIGN KEY (deal_id) REFERENCES deal(id),
FOREIGN KEY (product_uuid) REFERENCES product(uuid)
);
CREATE TABLE `order` (
uuid BINARY(16) NOT NULL PRIMARY KEY,
order_status VARCHAR(255) NOT NULL,
user_id BIGINT NOT NULL,
out_of_stock_handling_option VARCHAR(255) NOT NULL,
deal_product_uuid BINARY(16) NOT NULL,
original_order_quantity_for_partial_order INT,
real_order_quantity INT NOT NULL,
recipient_name VARCHAR(255) NOT NULL,
recipient_phone_number VARCHAR(255) NOT NULL,
recipient_address VARCHAR(255) NOT NULL,
recipient_detail_address VARCHAR(255),
shipping_request VARCHAR(255),
total_payment_amount INT,
payment_method VARCHAR(255),
payment_key VARCHAR(255),
payment_approve_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_at TIMESTAMP,
deleted_at TIMESTAMP
);
CREATE TABLE product (
uuid BINARY(1) NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
main_img_url VARCHAR(255) NOT NULL,
price INT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_at TIMESTAMP,
deleted_at TIMESTAMP
);
CREATE TABLE product_detail_img (
uuid BINARY(1) NOT NULL PRIMARY KEY,
product_uuid BINARY(1) NOT NULL,
url VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL,
deleted_at TIMESTAMP
);
CREATE TABLE time_deal_product_inventory_event_history (
inventory_event_history_id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
deal_product_uuid BINARY(16) NOT NULL,
order_uuid BINARY(16) NOT NULL,
inventory INT NOT NULL,
previous_deal_quantity INT NOT NULL,
inventory_event_type VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user (
id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
login_id VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
phone_number VARCHAR(255) NOT NULL,
address VARCHAR(255) NOT NULL,
detail_address VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_at TIMESTAMP,
deleted_at TIMESTAMP
);API 문서
프로젝트하면서 중요시 했던 포인트
- 다양하고 새로운 기술보다
문제에 집중해서 오버엔지니어링 하지 않기 - 테스트 코드 작성
- 유지보수성을 고려한 코드
- 확장성을 고려한 코드 및 설계
- 고가용성을 고려한 시스템 설계 및 구현(진행 중)
- 비동기로 진행되는 리뷰 상황에 대해 효율적인 소통을 위해 Github의 다양한 기능(이슈, 주석, 코멘트, PR)을 활용하여 문서화
- PR에 이 작업이 무엇인지, 왜 그렇게 했는지 등 맥락을 전달하기 위해 노력했습니다.
포인트 PR
-
테스트 코드로비즈니스 로직의 문서화를 위해서비스 클래스가 아닌도메인 모델에게비즈니스 로직 책임변경(#167) -> 굳이 로직 옮기 protected -
가독성과유지보수성을 고려해서fixture과함수활용해서 테스트 코드 리팩토링(#146) -
유지보수성을 고려해서 재고 증가/감소 및 재고 히스토리 저장 로직을응집력있게 묶어서 관리(#158) -
유지보수성을 고려해서 RedisUtils 클래스를 만들어서 Redis의 key를 한 곳에서응집력있게 관리 (#153) -
유연한 코드를 위해의존성 주입을 활용하여 인증 로직 구현(#171) -
트랜잭션이 필요한 구조 및 로직을
영속성이 필요 없는 필드를 제거하고새로운 클래스를 만들어서 트랜잭션 필요 없는 구조로 변경(#72)
프로젝트 진행하면서 했던 고민 포인트
1. 딜 상품과 재고를 통합형 구조로 Redis에 저장할까? 분리형 구조로 저장할까?
2. 주문 처리로 인한 재고 차감에서 발생하는 동시성 이슈 문제를 어떻게 해결할까?
3. 부분 주문 처리가 된 경우, 실제 사용자가 요청한 주문 수량과 실제 주문 처리 가능한 수량이 달라진다. CS(고객 서비스) 대응을 위해 실제 사용자가 요청한 주문 수량과 실제 주문 처리 가능한 수량 모두 DB에 저장되어 있어야 하는데, 어떻게 저장할까?
3. 재고를 Redis에서 관리하고 있지만, Redis에 장애가 날 경우 등을 대비하여 백업용으로 MySQL에도 저장해야 한다. 어떻게 저장할까?
- 주문 처리가 될 때마다 deal_product 테이블의 inventory를 업데이트 시키면, DB 병목이 발생할 것으로 예상되고, 또한, 다중 서버 환경에 의해 동시성 이슈 문제도 발생하기 때문에, 이를 해결하기 하기 위해 Lock 방식을 사용하게 될 경우, TPS도 낮아질 것이라고 예상된다. 어떻게 저장해야 할까?
Q) 주문 처리에 따라 재고 차감 로직에서 데이터 일관성과 정합성 불일치 문제를 어떻게 해결할 것인가? : JPA 낙관적 락
- 주문 처리에 따른 재고 차감 로직에서 데이터 일관성과 정합성 불일치 문제를 분산락 대신
재고 사후 검증단계를 도입함으로써 해결(재고 차감 및 실제 주문 수량 계산 로직 #136)
Q) 부분 주문의 경우, 사용자가 요청한 주문 수량과 실제 주문 처리 수량이 다른데, 어떻게 주문 수량을 관리할 것인가?
Q) 주문 관련 데이터(예 : 주문, 배송, 결제, 재고)를 일체형 주문 스키마 구조로 할 것인가, 분리형 주문 스키마 구조로 할 것인가
Q) 딜 상품과 재고를 통합해서 저장할 것인가? 분리해서 저장할 것인가?
기술적 고민 및 해결 포인트 -> 트러블 슈팅 과정에서 스프링 내부 동작원리에 대한 깊이있게 진행한 학습
Q) 여러 결제 API 중 어떤 결제 모듈을 사용할 것인가?
- 토스페이먼츠 vs 아임포트
Q) Redis에서 key 구조가 바뀔 때마다 수정이 필요한 곳을 개발자가 일일히 찾아서 바꿔야 하는 번거로움을 어떻게 해결할 것인가?
Q) 스키마가 바뀔 때마다 일일히 DB 스키마 SQL 실행시키는 번거로움을 어떻게 해소할 것인가?
- migration tool인 flyway 사용
Q) 재고를 API마다 다른 서버(상품 목록 API, 상품 상세보기API: Slave, 주문 API : Master)에서 가져오는 이유
- API 마다 일관성 수준을 달리해도 될 것 같아서
Q) ERR DISCARD without MULTI 이슈
Q) NotSerializableException 이슈
Q) UnnecessaryStubbingException 이슈
Q) Strict stubbing argument mismatch 이슈
Q) Flyway checksum 불일치 이슈
Q) java: builder() in xxx.xxx.DealProductSummaryForUI cannot hide builder() in xxx.xxx.DealProductSummary 이슈
프로젝트 진행하면서 깨달은 점
- 왜 로그가 필요한지
- 왜 int가 있는데 Integer가 필요한지
- 꼭 정규화만이 답이 아니다.
Metadata
Metadata
Assignees
Labels
No labels

