항해플러스 백엔드 4기 과제
E-Commerce 서비스를 제공하는 애플리케이션 구축
요구사항을 분석하여 프로젝트 설계 및 개발 진행
테스트 작성이 가능하고 유연한 아키텍처 구성
- Spring Boot 3.2.4
- Java 17
- Gradle 8.7
- JPA
- JUnit + AssertJ
- H2 Database
- Swagger 화면
- 상품
- 주문
- 사용자 지갑
- 장바구니
erDiagram
USER {
user_id BIGINT PK
name varchar(100)
address varchar(255)
}
WALLET ||--O{ WALLET_HISTORY : history
WALLET {
id bigint PK
user_id bigint UK
balance bigint
created_at datetime
update_at datetime
}
WALLET_HISTORY {
id bigint PK
user_id bigint
type varchar(10)
amount bigint
created_at datetime
}
ORDER ||--|{ORDER_ITEM: order
ORDER {
order_id UUID PK
user_id bigint
total_price bigint
product_quantity integer
address varchar(255)
payment_type varchar(20)
transaction_id varchar(36)
orderAt datetime
}
PRODUCT ||--o{ ORDER_ITEM: is
ORDER_ITEM {
order_id bigint PK
product_id bigint PK
order_price bigint
order_quantity integer
order_status varchar(20)
}
INVENTORY ||--|| PRODUCT: is
INVENTORY {
product_id bigint PK
stock integer
created_at datetime
update_at datetime
}
PRODUCT ||--o{ CART_ITEM: is
PRODUCT {
product_id bigint PK
name varchar(100)
price bigint
updated_at datetime
created_at datetime
}
CART_ITEM {
user_id bigint PK
product_id bigint
quantity integer
created_at datetime
update_at datetime
}
- 주문시 외부 API 요청에 따라 응답 시간이나 트랜젝션이 길어지는 문제 발생
- 주문은 성공했지만 외부 API가 실패하면 주문이 실패되는 상황이 발생하게 된다.
- 외부 API를 비동기로 처리하여 데이터를 보내는 역할만 할 수 있도록 리펙토링
@Async @Override public void send(OrderRequest request) { log.debug("call external api"); // 외부 API 호출 try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { throw new RuntimeException(e); } }
-
요구사항
- 상품 아이디로 상품 정보 및 잔여수량을 조회한다.
-
시퀀스 다이어그램
sequenceDiagram actor client participant app as application participant product as database(product) participant inventory as database(inventory) client->>app: GET /products/{id} activate app app->>product: 상품 조회 activate product alt has product product-->>app: 상품 데이터 else has not product product-->>app: NotFoundException app-->>client: 400 bad request end deactivate product app->>inventory: 재고 조회(상품 id) activate inventory alt has inventory inventory-->>app: 상품 재고 데이터 else has not inventory inventory-->>app: 재고 수량 0 end deactivate inventory app-->>client: 200 OK<br>(재고가 포함된 상품 정보) deactivate app
Endpoint
GET http://{server_url}/products/{id}
Request
Path Variable
파라미터 | 타입 | 필수여부 | 설명 |
---|---|---|---|
id | integer | Y | 상품 아이디 |
Response
Response Code
200 OK
400 bad request
Response body
파라미터 | 타입 | 필수여부 | 설명 |
---|---|---|---|
productId | integer | Y | 상품 아이디 |
name | string | Y | 상품 이름 |
price | integer | Y | 상품 가격 |
�stock | integer | Y | 잔여수량 |
프로세스
- 상품 아이디로 상품 테이블에서 데이터 조회
- 상품 아이디가 없을 경우 Exception 발생
- 상품이 존재할 경우
response
로 변환해서 반환함
CURL
curl --location --request GET 'http://localhost:8080/products/1'
-
요구사항
- 사용자 식별자와 (상품 ID, 수량) 목록을 입력받아 주문하고 결제를 수행
- 결제는 기 충전된 잔액을 기반으로 수행하며 성공할 시 잔액을 차감해야 합니다.
- 데이터 분석을 위해 결제 성공 시에 실시간으로 주문 정보를 데이터 플랫폼에 전송해야 합니다.
-
시퀀스 다이어그램
sequenceDiagram actor client participant app as application participant inventory participant order participant wallet participant walletHistory client->>app: POST /orders activate app app->>wallet: 총 결제 금액으로 지갑에서 차감 activate wallet alt 총 결제 금액 <= 잔액 wallet->>walletHistory:지갑 사용 이력 저장 else 총 결제 금액 > 잔액 wallet-->>client:400 bad request(BalanceException) end deactivate wallet loop order item app->>inventory:상품 재고 조회 activate inventory inventory-->>app:상품 재고 app->>app:상품 재고 차감 시도 alt 현재 재고 < 주문한 상품 수량 app-->>client:400 bad request(OutOfStockException) end app->>app:주문 아이템 생성 app->>inventory:차감된 재고 적용 end deactivate inventory app->>app: 주문생성 app->>order:주문 저장 app-->>client: 200 OK deactivate app
Endpoint
POST http://{server_url}/orders
Request
Request Body
파라미터 | 타입 | 필수여부 | 설명 |
---|---|---|---|
userId | string | Y | 사용자 아이디(uuid) |
totalPrice | integer | Y | 총 결제 금액 |
paymentType | string | Y | 결제 타입(WALLET ) |
orderAt | datetime | Y | 결제 일시 |
orderItems | Array | Y | 주문 상품 리스트 |
orderItems[].productId | integer | Y | 상품 아이디 |
orderItems[].price | price | Y | 결제 금액 |
orderItems[].quantity | quantity | Y | 결제 수량 |
Response
Response Code
200 OK
프로세스
- 준문 상품별 재고 수량 감소
- 남아있는 재고 수량을 초과하는 주문은 할 수 없다.
- 주문 요청에 총 결제 금액으로 지갑에서 차감 시도
- 잔액이 부족하면
Exception
발생
- 잔액이 부족하면
- 성공일 경우 비동기(
@Async
)를 활용해 데이터 플랫폼에 결제 정보 전송 200
응답
CURL
curl --location --request POST 'http://localhost:8080/orders' \
--header 'Content-Type: application/json' \
--data-raw '{
"userId":1,
"totalPrice":10000,
"paymentType":"WALLET",
"orderAt":"2024-04-04T15:24:54",
"orderItems": [
{
"productId":1,
"price":5000,
"quantity":2
},
{
"productId":2,
"price":5000,
"quantity":1
}
]
}'
- 요구사항
- 최근 3일간 가장 많이 팔린 상위 5개 상품 정보를 제공하는 API 작성.
- 나중에 추천 타입이 추가될 경우를 대비해서 추천타입 사용 :
type
- 순위에 따라서 오름차순으로 정렬 되어야 한다.
- TODO : 통계 테이블을 만들어서 적용해보기
- 시퀀스 다이어그램
sequenceDiagram actor client participant app as application participant orderItem as database<br>(order_item) participant product as database<br>(product) client->>app: GET /orders/{recommed_type} activate app app->>orderItem: 추천 타입별 상품 조회 orderItem-->>app: 추천된 상품 정보 loop 추천된 상품 app->>product: 상품별 메타정보 데이터 조회 product-->>app: 상품별 메타정보 맵핑(이름, 옵션...) end app-->>client: 200 OK<br>(추천된 상품 정보) deactivate app
Endpoint
GET http://{server_url}/orders/{type}
Request
Path Variable
파라미터 | 타입 | 필수여부 | 설명 |
---|---|---|---|
type | string | Y | 추천 타입(RECOMMEND_01 ) |
Response
Response Code
200 OK
Response body
파라미터 | 타입 | 필수여부 | 설명 |
---|---|---|---|
[].rank | integer | Y | 순위 |
[].productId | integer | Y | 상품 아이디 |
[].price | integer | Y | 상품 가격 |
[].orderCount | integer | Y | 주문 수량 |
프로세스
type
별 추천 로직 실행- 조회할때 캐시를 등록 (반복되는 Sql 쿼리 실행을 막음)
- 주문시 해당 캐시 키 삭제
CURL
curl --location --request GET 'http://localhost:8080/orders/RECOMMEND_01'
- 요구사항
- 사용자 식별자를 통해 해당 사용자의 잔액을 조회한다.
- 시퀀스 다이어그램
sequenceDiagram actor client participant app as application participant user participant wallet client->>app:GET /wallet/users/{key} activate app app->>user:사용자 정보 조회 alt 사용자 정보가 존재하지 않을 경우 user-->>app:NotFoundException app-->>client:400 bad request else 사용자 정보가 존재할 경우 user-->>app:사용자 정보 반환 end app->>wallet:지갑 테이블 조회 alt 사용자에 대한 지갑 정보가 존재하지 않을 경우 wallet-->>app:잔액 0으로 반환 else 지갑 정보가 존재할 경우 wallet-->>app:지갑 정보 반환 end app-->>client:200 OK<br>(지갑 정보) deactivate app
Endpoint
GET http://{server_url}/wallet/users/{id}
Request
Path Variable
파라미터 | 타입 | 필수여부 | 설명 |
---|---|---|---|
id | string | Y | 사용자 아이디(uuid) |
Response
Response Code
200 OK
Response body
파라미터 | 타입 | 필수여부 | 설명 |
---|---|---|---|
userId | string | Y | 사용자 아이디(uuid) |
balance | integer | Y | 잔액 |
updateAt | integer | N | 마지막 업데이트 일시 |
프로세스
- 사용자 지갑 테이블 조회
- 만약 존재하지 않으면 잔액을 0으로 입력후 return
- 조회된 데이터 또는 0으로 입력된 객체 응답
CURL
curl --location --request GET 'http://localhost:8080/wallet/users/1'
- 요구사항
- 사용자 식별자 및 충전할 금액을 받아 잔액 충전
- 시퀀스 다이어그램
sequenceDiagram actor client participant app as application participant wallet as database<br>(wallet) client->>app:PATCH /wallet/charge activate app app->wallet:지갑 테이블 조회 wallet-->app:지갑 정보 alt 사용자에 대한 지갑 정보가 존재하지 않을 경우 app->>wallet: 충전 금액으로 지갑 데이터 저장 else 지갑 정보가 존재할 경우 app->>wallet: 지갑 잔액에 입력받은 금액 추가 end app-->>client:200 OK deactivate app
Endpoint
PATCH http://{server_url}/wallet/charge
Request
Request Body
파라미터 | 타입 | 필수여부 | 설명 |
---|---|---|---|
userId | string | Y | 사용자 아이디(uuid) |
amount | integer | Y | 충전 금액 |
Response
Response Code
200 OK
프로세스
- 사용자 지갑 테이블 조회(해당 지갑에
Lock
사용) - 지갑 데이터가 존재 할 경우
- 사용자의 잔액에 입력받은 금액을 추가하여
update
- 사용자의 잔액에 입력받은 금액을 추가하여
- 데이터가 존재하지 않을 경우
- 충전 금액으로 지갑 데이터 추가
CURL
curl --location --request PATCH 'http://localhost:8080/wallet/charge' \
--header 'Content-Type: application/json' \
--data-raw '{
"userId":1,
"amount":10000
}'
- 요구사항
- 사용자가 장바구니에 상품을 추가하는 API
- 수량에
0
이 들어올 수 없다. - 사용자별 상품은 하나만 장바구니에 추가 할 수 있다.
- 시퀀스 다이어그램
sequenceDiagram actor client participant app as application participant cart as database<br>(cart_item) client->>app: PUT /carts activate app app->>cart: 장바구니 데이터 저장<br>(insert or update) app-->>client: 200 OK deactivate app
Endpoint
PUT http://{server_url}/carts
Request
Request Body
파라미터 | 타입 | 필수여부 | 설명 |
---|---|---|---|
userId | string | Y | 사용자 아이디 |
productId | integer | Y | 상품 아이디 |
quantity | integer | Y | 상품 수량 |
eventAt | datetime | Y | 이벤트 발생 시간 |
Response
Response Code
200 OK
프로세스
- 요청으로 들어온 장바구니 상품 수량을 저장
- 상품 수량이
0
으로 들어올 경우 장바구니에서 데이터 삭제
CURL
curl --location --request PUT 'http://localhost:8080/carts' \
--header 'Content-Type: application/json' \
--data-raw '{
"userId":1,
"productId":1,
"quantity":10,
"eventAt":"2024-04-05T10:55:17"
}'
- 요구사항
- 장바구니에 상품을 삭제하는
- 시퀀스 다이어그램
sequenceDiagram actor client participant app as application participant cart as database<br>(cart_item) client->>app: DELETE /carts/users/{userId}/products/{productId} activate app app->>cart: 장바구니 데이터 삭제 app-->>client: 200 OK deactivate app
Endpoint
DELETE http://{server_url}/carts/users/{userId}/products/{productId}
Request
Path Variable
파라미터 | 타입 | 필수여부 | 설명 |
---|---|---|---|
userId | string | Y | 사용자 아이디 |
productId | integer | Y | 상품 아이디 |
Response
Response Code
200 OK
프로세스
- 사용자 아이디와 상품 아이디로 장바구니 테이블에 상품을 삭제한다.
CURL
curl --location --request DELETE 'http://localhost:8080/carts/users/1/products/1'
- 요구사항
- 특정 사용자의 장바구니에 담긴 상품을 조회
- 조회할때 상품 이름과 가격도 같이 조회한다.
- 시퀀스 다이어그램
sequenceDiagram actor client participant app as application participant cart as database<br>(cart_item) participant product as database<br>(product) client->>app: GET /carts/users/{userId} activate app app->>cart: 특정 사용자 장바구니 데이터 조회 cart-->>app: 장바구니 데이터 loop cart item app->>product: 장바구니 상품 조회 product-->>app: 장바구니 상품 메타데이터 맵핑 end app-->>client: 200 OK<br>(장바구니 상품 목록) deactivate app
Endpoint
GET http://{server_url}/carts/users/{userId}
Request
Path Variable
파라미터 | 타입 | 필수여부 | 설명 |
---|---|---|---|
userId | string | Y | 사용자 아이디 |
Response
Response Code
200 OK
Response body
파라미터 | 타입 | 필수여부 | 설명 |
---|---|---|---|
[].productId | integer | Y | 상품 아이디 |
[].productName | string | Y | 상품 이름 |
[].quantity | integer | Y | 장바구니 상품 수량 |
[].price | integer | Y | 상품 가격 |
[].lastUpdateAt | datetime | Y | 마지막 업데이트 일시 |
프로세스
- 사용자 아이디를 받아서 장바구니를 조회한다.
- 장바구니에 상품별로 이름과 가격을 상품 테이블에서 조회한다.
- 조회된 장바구니 데이터와 상품 데이터를 조합하여 응답한다.
CURL
curl --location --request GET 'http://localhost:8080/carts/users/1'