Skip to content

주문 서버의 관심사 작성 및 주문 API 작성 #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bfe706e
📝 Docs : 문서 수정
haxr369 Apr 17, 2024
11fb85e
✨ Feat : 클라이언트 주문 요청 API 작성
haxr369 Apr 18, 2024
b910033
✨ Feat : ProductService 재고 확인 기능 추가
haxr369 Apr 18, 2024
8fd7f7e
✨ Feat : 새로운 기능
haxr369 Apr 18, 2024
168ea41
✨ Feat : 주문 생성 및 유형 제품과 연결 시도
haxr369 Apr 19, 2024
b200e12
🐛 Fix : 데이터 생성 문제 해결
haxr369 Apr 19, 2024
158732b
✨ Feat : 핵심제품에 재고량 확인 가능하도록 수정
haxr369 Apr 20, 2024
ae1b0b0
✅ Test : 핵심제품 재고 업데이트 테스트 작성
haxr369 Apr 20, 2024
b474fa9
✅ Test : 여러 transaction에서 동시에 한 레코드를 수정할 때 동시성 문제 발생시키기
haxr369 Apr 21, 2024
f2ab0da
✅ Test : 비관적락 낙관적락 전략 적용
haxr369 Apr 22, 2024
59997e0
✨ Feat : 낙관적 락의 예외를 처리할 수 있도록 메서드 추가
haxr369 Apr 22, 2024
9ffdf64
🤖 Refactor : 응답을 ResponseEntity를 상속해서 헤더를 사용할 수 있게함. 추후 리다이렉트 사용할 때 용…
haxr369 Apr 23, 2024
b6cdd0c
🤖 Refactor : ResponseEntity를 주로 사용하도록 응답 수정
haxr369 Apr 23, 2024
bd4ecad
✨ Feat : 결제 서버 연결할 준비
haxr369 Apr 23, 2024
9b5d1f3
✨ Feat : 결제 요청 기능 구현
haxr369 Apr 23, 2024
a35bf01
✨ Feat : 결제 결과 요청하도록 confirm 수정
haxr369 Apr 24, 2024
f7f8a90
✨ Feat : 결제 결과 받아서 주문 확정 시작
haxr369 Apr 26, 2024
367a68f
✨ Feat : 새로운 기능
haxr369 Apr 27, 2024
ab157a0
✅ Test : 분산락 이용해서 재고 감소를 락을 가진 쓰레드만 할 수 있게함
haxr369 Apr 28, 2024
62816e2
✅ Test : 분산락에 대한 테스트 구현
haxr369 Apr 28, 2024
bf35b61
✨ Feat : 롤백 로직 추가 완료
haxr369 Apr 28, 2024
1168d6c
✨ Feat : 수량을 일정 개수 미만으로 주문하면 정상적으로 롤백 할 수 있도록 로직 작성
haxr369 Apr 29, 2024
e849c9b
🐛 Fix : 분산락 이용해서 서로 다른 세션에서 동시성 문제 해결
haxr369 Apr 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
.env.local
.gitignore
README.md
.gitmessage
.gitmessage.txt
LICENSE
SunStyle_edited.xml
codecov.yml
Expand Down
File renamed without changes.
69 changes: 68 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,73 @@
![Codecov](https://img.shields.io/badge/Codecov-F01F7A?logo=codecov&logoColor=white)
![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-2088FF?logo=githubactions&logoColor=white)

# 관심사
# 주문 서버 관심사
### 주문 및 결제 과정

1. 클라이언트는 주문 서버에 주문 요청
2. 주문 서버는 클라이언트 요청의 유효성을 검사
1. `PENDING_ORDER` 상태의 유형제품을 선택
2. 유형제품의 상태를 `PROCESSING`로 변경
3. 주문에 유형 제품을 추가
4. 만약 주문 요청한 수량만큼 `PENDING_ORDER` 상태의 유형제품이 없다면, `재고가 부족합니다. ` 문구를 클라이언트에게 응답
3. 주문 서버가 결제 서버에 결제 요청
4. 결제 서버가 결제 요청 받았다고 응답
5. 주문 서버는 결제 요청이 들어간 것을 클라이언트에게 전달
6. 결제 서버가 결제 결과를 주문 서버에 전달
1. 결제가 정상적으로 완료되면,
1. 유형제품의 상태를 `SHIPPING`으로 변경
2. 핵심제품의 재고를 차감
2. 결제가 실패되면,
1. 재고 유지
2. 주문의 유형제품 상태를 `PENDING_ORDER`로 변경
7. 주문 서버는 받는 결과를 클라이언트에게 전달

### 클라이언트 주문 요청 형태

```json
{
"core_products" : {
"1" : "30",
"2" : "10"
},
"client_type" : "InexperiencedCustomer",
"payment_method" : "CREDIT_CARD"
}
```

### 결제 요청 형태
```json
{
"buyer": {
"name": "John Doe",
"email": "[email protected]"
},
"seller": {
"name": "Jane Doe",
"email": "[email protected]"
},
"payment": "CREDIT_CARD",
"price": 100,
"redirect": "http://localhost:8080/payment/confirm"
}
```

### 상품의 상태에 대한 설명

`PENDING_ORDER` : 유형제품이 주문에 포함되지 않는 기본적인 상태를 의미. 여러 주문에서 동시에 접근하면 한 주문에만 들어간다.

`PROCESSING` : 유형제품이 주문에 포함되며, 결제 결과를 기다리는 상태를 의미.

`SHIPPING` : 결제가 정상적으로 종료되고, 해당 유형제품이 온전히 고객의 소유가 되는 상태.

`DELIVERED` : 상품이 고객에게 배송 완료된 상태. 현재 프로젝트에서는 배송까지는 관심사가 아니기 때문에 사용하지 않는다.

### 클라이언트 요청 유효성 검사 리스트

- [ ] 핵심제품 id가 존재하는가?
- [ ] 핵심제품의 재고가 충분한가?
- [ ] `PENDING_ORDER` 상태의 유형제품이 충분한가?
- [ ] 클라이언트가 상품 구매 권한이 있는가?
- [ ] 결제 방식이 유효한 방식인가?

# 정보
10 changes: 10 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ repositories {
}

@Suppress("SpellCheckingInspection") dependencies {
// spring-distribute-lock
implementation("org.springframework.integration:spring-integration-jdbc")
implementation("org.springframework.boot:spring-boot-starter-jdbc")
implementation("org.springframework.boot:spring-boot-starter-integration")

// spring-web-payment
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.5.0")
runtimeOnly("io.netty:netty-resolver-dns-native-macos:4.1.94.Final:osx-aarch_64")

// spring-web-jpa-concurrency
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
Expand Down
24 changes: 12 additions & 12 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,22 @@ services:
depends_on:
mysql:
condition: service_healthy
networks:
- private-subnet
# networks:
# - private-subnet

mysql:
<<: *mysql-template
hostname: dev
container_name: mysql-dev
ports:
- "3306:3306"
networks:
- private-subnet

networks:
private-subnet: # private: 172.19.0.x
driver: bridge
ipam:
driver: default
config:
- subnet: 172.19.0.0/24
# networks:
# - private-subnet
#
#networks:
# private-subnet: # private: 172.19.0.x
# driver: bridge
# ipam:
# driver: default
# config:
# - subnet: 172.19.0.0/24
14 changes: 14 additions & 0 deletions src/main/java/com/concurrency/config/EnumMappingConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.concurrency.config;

import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class EnumMappingConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
ApplicationConversionService.configure(registry);
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/concurrency/config/JDBCConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.concurrency.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.jdbc.lock.DefaultLockRepository;
import org.springframework.integration.jdbc.lock.JdbcLockRegistry;
import org.springframework.integration.jdbc.lock.LockRepository;

import javax.sql.DataSource;

@Configuration
public class JDBCConfig {
@Bean
public DefaultLockRepository DefaultLockRepository(DataSource dataSource){
return new DefaultLockRepository(dataSource);
}
@Bean
public JdbcLockRegistry jdbcLockRegistry(LockRepository lockRepository){
return new JdbcLockRegistry(lockRepository);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
package com.concurrency.jpa.customer.Product;

import com.concurrency.jpa.customer.Product.entity.ActualProduct;
import com.concurrency.jpa.customer.Product.enums.ActualStatus;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface ActualProductRepository extends JpaRepository<ActualProduct, Long>{
@Query(value = "SELECT count(a.id) FROM ActualProduct a " +
"WHERE a.coreProduct.id = :coreId AND a.actualStatus = :actualStatus")
Long countByCoreProductIdANDActualStatus(@Param("coreId") Long k, @Param("actualStatus") ActualStatus actualStatus);

// @Query(value = "SELECT a FROM ActualProduct a " +
// "WHERE a.coreProduct.id = :coreProductId AND a.actualStatus = :reqStatus")
List<ActualProduct> findByCoreProduct_IdAndActualStatus(@Param("coreProductId") Long coreProductId, @Param("reqStatus")ActualStatus reqStatus, Pageable pageable);
List<ActualProduct> findByOrder_Id(Long orderId);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
package com.concurrency.jpa.customer.Product;

import com.concurrency.jpa.customer.Product.entity.CoreProduct;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface CoreProductRepository extends JpaRepository<CoreProduct, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select c from CoreProduct c where c.id = :id" ) // 왜 findByTicketName는 직접 못할까?
Optional<CoreProduct> findByIdPessimistic(@Param("id") Long id);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.concurrency.jpa.customer.Product.dto;

import lombok.Getter;
import lombok.Value;

@Value
@Getter
public class StockDto {
Long stock;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.concurrency.jpa.customer.Product.entity;


import com.concurrency.jpa.customer.Product.enums.ActualStatus;
import com.concurrency.jpa.customer.order.Order;
import com.concurrency.jpa.customer.order.dto.ActualProductDto;
import jakarta.persistence.*;
import lombok.*;

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "actual_product")
public class ActualProduct {
@Id
@Getter
@Column(name = "actual_product_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Enumerated(EnumType.STRING)
@Column(name = "actual_status", columnDefinition = "varchar(255)")
private ActualStatus actualStatus;

@ManyToOne
@JoinColumn(name = "core_product_id", nullable = false)
private CoreProduct coreProduct;

@ManyToOne
@Setter
@JoinColumn(name = "order_id")
private Order order;

@Getter
@Column(name = "actual_price")
private Long actualPrice;

@Column(name = "discount_rate")
private float discountRate;

public void updateActualProductStatus(ActualStatus actualStatus){
this.actualStatus = actualStatus;
}

public ActualProductDto toDto(){
return ActualProductDto.builder()
.actualProductId(id)
.actualStatus(actualStatus)
.coreProductId(coreProduct.getId())
.actualPrice(actualPrice)
.discountRate(discountRate)
.build();
}

public Long getCoreProductId(){
return coreProduct.getId();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.concurrency.jpa.customer.Product;
package com.concurrency.jpa.customer.Product.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;


Expand All @@ -13,11 +14,20 @@
@Table(name = "core_product")
public class CoreProduct {
@Id
@Getter
@Column(name = "core_product_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long price;
@Version
private Long version;
@Getter
private Long stock;
@Column(name = "seller_id")
private Long sellerId;

public long addStrock(Long change){
stock += change;
return stock;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.concurrency.jpa.customer.Product.enums;

public enum ActualStatus {
PENDING_ORDER,
PROCESSING,
SHIPPING,
DELIVERED,
FAILED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.concurrency.jpa.customer.common;

import lombok.Getter;

@Getter
public class BaseException extends RuntimeException{

private final BaseResponseStatus status;

public BaseException(BaseResponseStatus status) {
super(status.getMessage());
this.printStackTrace();
this.status = status;
}

}
Loading
Loading