- 클라이언트에서 예외 처리 로직을 일관되게 구현할 수 있도록 ErrorResponse를 설계했습니다.
@Valid
에서 예외 발생 시 fieldError에서 예외가 발생한 필드를 ErrorResponse에 담아 정확한 예외 메시지를 클라이언트에 전달하고자 했습니다.- 어플리케이션에서 발생한 예외는 CustomException을 정의한 후 ErrorCode Enum을 통해 예외를 관리했습니다.
@ControllerAdvice
를 통해 Custom 예외와@Valid
에서 발생하는 예외를 핸들링 했습니다.
- 조회의 경우 수많은 데이터가 있을 것으로 예상되어 페이징 처리를 하여 성능 향상을 도모했습니다.
- 페이징 처리 시 fetch join을 사용하면 모든 데이터를 가져온 후 어플리케이션 레벨에서 페이징 하기 때문에 성능 저하가 발생하기 때문에 inner join 후
spring.jpa.default_batch_fetch_size
를 통해 해결했습니다. - 상품 조회 시 cache를 적용했습니다. 인자값으로 LocalDateTime에서 오늘 날짜와 Pageable의 페이지 번호를 결합한 키를 만들어 캐싱을 했습니다.
- 데이터 정합성을 위해서 cacheConfig 시 1분마다 캐시를 삭제하도록 했습니다.
- 비즈니스 로직을 엔티티에서 처리하도록 해서 객체지향적인 코드를 작성하고자 했습니다.
- 서비스 레이어에서는 Transaction, Cache 적용과 같은 infra를 적용하는데 집중하도록 했습니다.
- 상품 조회 시 캐시를 사용하여 대량 트래픽에서 효율적으로 응답을 반환하도록 했습니다.
- 여러 사용자가 동시 주문 시 동시성 문제를 pessimistic lock으로 해결했습니다.
- pessimistic lock의 작동을 확인하기 위해 테스트 코드를 작성했습니다.
기능 | API |
---|---|
상품 조회 | GET /api/products |
상품 주문 | POST /api/orders |
상품 주문 취소 | POST /api/orders/{order_id}/cancel |
주문 내역 조회 | GET /api/orders |
예외 코드 | 내용 |
---|---|
A001 | 유효하지 않은 ProductId 요청 시 발생하는 예외 |
A002 | 유효하지 않은 OrderId 요청 시 발생하는 예외 |
B001 | 상품 주문 시 상품의 재고가 부족하면 발생하는 예외 |
B002 | 상품 주문 시 요청의 금액이 상품의 금액보다 작으면 발생하는 예외 |
B003 | 상품 주문 시 상품의 상태가 판매 중지된 상품이면 발생하는 예외 |
C001 | 상품 취소 시 주문이 이미 완료된 상태일 때 발생하는 예외 |
C002 | 상품 취소 시 주문이 이미 취소된 상태일 때 발생하는 예외 |
C003 | 상품 취소 시 취소 금액과 총 주문 금액이 일치하지 않을 때 발생하는 예외 |
D001 | 입력 파라미터가 잘못된 경우 발생하는 예외 |
◀️ Click! - 상품 조회 요청 및 응답 예시
기능
- display_date 기준으로 전시중인 상품을 페이징 처리하여 반환한다.
API
GET /api/products
Query Param
- Required
- None
- Option
- display_date
- display_date=2022-08-21T00:00
- 미입력시 default: 현재시간
- page
- page=1
- 미입력시 default: page=0
- display_date
Error Response
-
display_date의 ISO Date Time Format이 잘못되었을 때
-
GET http://localhost:8080/api/products?display_date=2022-08-21T00&page=0
-
Http Status: 400
{
"errorInfo": {
"code": "D001",
"message": "요청이 올바르지 않습니다"
},
"fieldErrors": [
{
"field": "display_date",
"value": "2022-08-21T00",
"reason": "typeMismatch"
}
]
}
Success Response
GET http://localhost:8080/api/products?display_date=2022-08-21T00:00&page=1
- Http Status: 200
{
"content": [
{
"id": 6,
"name": "(아마존)Corsai 벤전스LPX DDR4 데스크톱 메모리 키트 16GB (2x8GB) 블랙(CMK16GX4M2B3200C16)",
"price": 84270,
"quantity": 30,
"sellerId": 3,
"sellerName": "하이닉스",
"status": "SALE"
},
{
"id": 7,
"name": "갤럭시S22",
"price": 1200000,
"quantity": 30,
"sellerId": 4,
"sellerName": "삼성",
"status": "SALE"
},
{
"id": 8,
"name": "갤럭시 워치 4",
"price": 220000,
"quantity": 60,
"sellerId": 4,
"sellerName": "삼성",
"status": "SALE"
},
{
"id": 9,
"name": "갤럭시S10",
"price": 1000000,
"quantity": 100,
"sellerId": 4,
"sellerName": "삼성",
"status": "SUSPENDED"
},
{
"id": 10,
"name": "갤럭시 버즈 프로",
"price": 330000,
"quantity": 100,
"sellerId": 4,
"sellerName": "삼성",
"status": "SALE"
}
],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"pageNumber": 1,
"pageSize": 5,
"offset": 5,
"paged": true,
"unpaged": false
},
"totalPages": 2,
"totalElements": 10,
"last": true,
"numberOfElements": 5,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"size": 5,
"number": 1,
"first": false,
"empty": false
}
특이 사항
- 캐시 적용
- spring.jpa.default_batch_fetch_size를 통한 N+1 쿼리 해결
◀️ Click! - 상품 주문 요청 및 응답 예시
기능
- 사용자가 상품을 주문하면 주문 수량만큼 상품의 재고가 감소하고 상품이 주문된다.
- 여러 가지 상품을 한 번의 주문에 주문할 수 있다.
API
POST /api/orders
Header
x-user-id:greatpeople
Request Body
{
"orders": [
{
"productId": 2,
"price": 800000,
"quantity": 1
},
{
"productId": 4,
"price": 110000,
"quantity": 1
}
],
"address": {
"city": "서울시 송파구",
"street": "송파대로 567",
"zipCode": "05503"
}
}
Error Response
- Request Body의 productId에 해당하는 상품이 없을 때
- Http Status: 400
{
"errorInfo": {
"code": "A001",
"message": "해당 상품이 존재하지 않습니다"
},
"fieldErrors": []
}
- Request Body의 수량보다 상품의 재고가 적을 때
- Http Status: 400
{
"errorInfo": {
"code": "B001",
"message": "재고가 부족합니다"
},
"fieldErrors": []
}
- Request Body의 금액보다 상품의 가격이 클 때
- Http Status: 400
{
"errorInfo": {
"code": "B002",
"message": "입금된 금액이 충분하지 않습니다"
},
"fieldErrors": []
}
- Request Body의 productId에 해당하는 상품이 판매 중지일 때
- Http Status: 400
{
"errorInfo": {
"code": "B003",
"message": "판매 중지된 상품입니다"
},
"fieldErrors": []
}
- Request Body의 값이 음수이거나 비어 있어 Valid에서 검증되는 경우
- Http Status: 400
{
"errorInfo": {
"code": "D001",
"message": "요청이 올바르지 않습니다"
},
"fieldErrors": [
{
"field": "orders[1].productId",
"value": "-9",
"reason": "0보다 커야 합니다"
},
{
"field": "address.city",
"value": "",
"reason": "비어 있을 수 없습니다"
},
{
"field": "orders[1].price",
"value": "-11000000",
"reason": "0보다 커야 합니다"
},
{
"field": "orders[1].quantity",
"value": "-1",
"reason": "0보다 커야 합니다"
}
]
}
Success Response
- 생성된 주문의 식별값을 반환
- Http Status: 201
{
"orderId": 8
}
특이 사항
- 비관적 락을 통해 상품의 수량을 감소시켜 동시성 문제 해결
- Entity에 비즈니스 로직을 넣어 Entity가 직접 자신의 정보를 수정하도록 함
◀️ Click! - 상품 주문 취소 요청 및 응답 예시
기능
- 사용자가 상품 주문 취소하면 상품의 재고가 원래대로 돌아가고 주문이 취소된다.
API
POST /api/orders/{order_id}/cancel
Request Body
{
"cancelPrice": 4040000
}
Error Response
- PathVariable의 orderId에 해당하는 주문이 없을 때
- Http Status: 400
{
"errorInfo": {
"code": "A002",
"message": "해당 주문이 존재하지 않습니다"
},
"fieldErrors": []
}
- PathVariable의 orderId에 해당하는 주문이 완료된 상태일 때
- Http Status: 400
{
"errorInfo": {
"code": "C001",
"message": "이미 완료된 주문은 취소가 불가능합니다"
},
"fieldErrors": []
}
- PathVariable의 orderId에 해당하는 주문이 이미 취소 상태일 때
- Http Status: 400
{
"errorInfo": {
"code": "C002",
"message": "이미 취소된 주문은 취소가 불가능합니다"
},
"fieldErrors": []
}
- Request Body의 취소 금액과 총 주문 금액이 일치하지 않을 때
- Http Status: 400
{
"errorInfo": {
"code": "C003",
"message": "취소 금액과 총 주문 금액이 일치하지 않습니다."
},
"fieldErrors": []
}
- Request Body의 값이 음수이거나 비어 있어 Valid에서 검증되는 경우
- Http Status: 400
{
"errorInfo": {
"code": "D001",
"message": "요청이 올바르지 않습니다"
},
"fieldErrors": [
{
"field": "cancelPrice",
"value": "-4040000",
"reason": "0 이상이어야 합니다"
}
]
}
Success Response
- 취소된 주문의 식별값을 반환
- Http Status: 200
{
"orderId": 1
}
특이 사항
- Entity에 비즈니스 로직을 넣어 Entity가 직접 자신의 정보를 수정하도록 함
◀️ Click! - 주문 내역 조회 요청 및 응답 예시
기능
- start_date, end_date 사이에 해당하는 회원의 주문 내역을 페이징 처리하여 반환한다.
API
GET /api/products
Header
x-user-id:greatpeople
Query Param
- Required
- start_date
- end_date
Error Response
- start_date 또는 end_date의 ISO Date Time Format이 잘못되었을 때
GET http://localhost:8080/api/orders?start_date=2022-06-20&end_date=2022-08-21T00:00
- Http Status: 400
{
"errorInfo": {
"code": "D001",
"message": "요청이 올바르지 않습니다"
},
"fieldErrors": [
{
"field": "start_date",
"value": "2022-06-20",
"reason": "typeMismatch"
}
]
}
- start_date가 end_date보다 이후의 Date Time 일 때
GET http://localhost:8080/api/orders?start_date=9999-12-20&end_date=2022-08-21T00:00
- Http Status: 400
{
"errorInfo": {
"code": "D001",
"message": "요청이 올바르지 않습니다"
},
"fieldErrors": []
}
Success Response
GET http://localhost:8080/api/orders?start_date=2022-06-20T00:00&end_date=2022-08-21T00:00
- Http Status: 200
{
"content": [
{
"orderId": 2,
"orderHistories": [
{
"productName": "문화상품권",
"productPrice": 50000,
"orderPrice": 500000,
"orderQuantity": 10
}
],
"address": {
"city": "서울시 송파구",
"street": "송파대로 567",
"zipCode": "05503"
}
},
{
"orderId": 3,
"orderHistories": [
{
"productName": "(아마존)Corsai 벤전스LPX DDR4 데스크톱 메모리 키트 16GB (2x8GB) 블랙(CMK16GX4M2B3200C16)",
"productPrice": 84270,
"orderPrice": 84270,
"orderQuantity": 1
}
],
"address": {
"city": "서울시 송파구",
"street": "송파대로 567",
"zipCode": "05503"
}
},
{
"orderId": 4,
"orderHistories": [
{
"productName": "아이패드",
"productPrice": 800000,
"orderPrice": 800000,
"orderQuantity": 1
},
{
"productName": "애플팬슬",
"productPrice": 110000,
"orderPrice": 110000,
"orderQuantity": 1
}
],
"address": {
"city": "서울시 송파구",
"street": "송파대로 567",
"zipCode": "05503"
}
},
{
"orderId": 5,
"orderHistories": [
{
"productName": "갤럭시 버즈 프로",
"productPrice": 330000,
"orderPrice": 330000,
"orderQuantity": 1
}
],
"address": {
"city": "서울시 송파구",
"street": "송파대로 567",
"zipCode": "05503"
}
},
{
"orderId": 6,
"orderHistories": [
{
"productName": "맥북프로",
"productPrice": 3400000,
"orderPrice": 3400000,
"orderQuantity": 1
}
],
"address": {
"city": "서울시 송파구",
"street": "송파대로 567",
"zipCode": "05503"
}
}
],
"pageable": {
"sort": {
"unsorted": true,
"sorted": false,
"empty": true
},
"pageNumber": 0,
"pageSize": 5,
"offset": 0,
"paged": true,
"unpaged": false
},
"totalPages": 1,
"totalElements": 5,
"last": true,
"numberOfElements": 5,
"number": 0,
"first": true,
"size": 5,
"sort": {
"unsorted": true,
"sorted": false,
"empty": true
},
"empty": false
}
특이 사항
- spring.jpa.default_batch_fetch_size를 통한 N+1 쿼리 해결
git clone https://github.com/jeremy0405/11st-assignment.git
cd 11st-assignment
chmod +x gradlew
./gradlew build
java -jar build/libs/elevenstreet-0.0.1-SNAPSHOT.jar
http://localhost:8080/h2-console
위와 같이 설정 후 Connect 하여 DB 확인 가능