Skip to content
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

[feat] 결제시 동일한 카트에 대한 중복 결제 문제 #150

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c2cf4d6
[feat] redis를 이용하여 cart를 저장/조회/삭제하도록 구현 (#134)
Hyeon-Uk Aug 22, 2024
8d43c2f
[feat] Cart를 DB에 저장한다 (#133)
Dr-KoKo Aug 22, 2024
0c1986e
[feat] 구매자는 카트를 조회할 수 있다 (#137)
Dr-KoKo Aug 23, 2024
d9c9816
[feat] CartEntity의 CustomerId 변경
Dr-KoKo Aug 23, 2024
32ecb91
[fix] CartDao 이중 등록 제거
Dr-KoKo Aug 23, 2024
bc161c8
[fix] CartItemEntity에 id가 null로 갱신되는 문제 해결
Hyeon-Uk Aug 23, 2024
f393d9c
[fix] 의존 주입할 Bean을 application.yml에서 관리할 수 있도록 수정
Hyeon-Uk Aug 23, 2024
24d8ccc
[feat] 중복 주문에 대한 Exception 생성 및 error code 갱신
Hyeon-Uk Aug 23, 2024
833fa6c
[test] 중복 주문 동시성 테스트에 사용할 더미데이터 만드는 메서드 추가
Hyeon-Uk Aug 23, 2024
e534e18
[test] 중복 주문 방지에 대한 동시성 테스트 시나리오 추가
Hyeon-Uk Aug 23, 2024
14edb4c
[refactor] 검증 부분 주석 추가
Hyeon-Uk Aug 23, 2024
8d70ebf
[fix] OneToMany 관계에서 cascade를 사용하기 위해 mappedBy 사용
Dr-KoKo Aug 24, 2024
04bb3a0
[feat] 마이크로미터 앤드포인트 노출
Dr-KoKo Aug 25, 2024
898034c
[feat] 마이크로미터 앤드포인트 노출
Dr-KoKo Aug 25, 2024
a308456
[test] 마이크로미터 앤드포인트 노출
Dr-KoKo Aug 25, 2024
9d998a5
[fix] APIResponseAdvice에 @EnableConfigurationProperties 추가
Dr-KoKo Aug 25, 2024
93e994d
[fix] JpaCartDao 오류 수정
Dr-KoKo Aug 25, 2024
a2b0da5
[feat] 주문 시점에 배타락을 적용해서 문제를 해결 (#148)
Dr-KoKo Aug 26, 2024
ddf9c3b
[feat] 주문 시점에 분산락을 적용해서 문제를 해결한 버전 (#147)
Hyeon-Uk Aug 26, 2024
fa6980e
Revert "[feat] 주문 시점에 배타락을 적용해서 문제를 해결 (#148)"
Dr-KoKo Aug 26, 2024
21f03ce
[fix] CartRepository를 트랜잭션 내에서 테스트
Dr-KoKo Aug 26, 2024
1cbb09a
[fix] cart 저장소를 redis로 변경
Dr-KoKo Aug 26, 2024
7247757
[fix] Redisson에 의존하는 configuration 이름을 redis로 변경
Hyeon-Uk Aug 27, 2024
ef4694a
[feat] conflict 해결
Hyeon-Uk Aug 27, 2024
ae43d0b
Merge remote-tracking branch 'refs/remotes/origin/main' into feature/…
Dr-KoKo Aug 28, 2024
d83b684
[fix] 생성자 통일
Dr-KoKo Aug 29, 2024
73bf5ef
[fix] redis 설정 통합
Dr-KoKo Aug 29, 2024
7fba4a7
[fix] 컴파일 오류 해결
Dr-KoKo Aug 29, 2024
12367ad
[fix] 테스트시 in_memory 버전의 CartRepository를 사용하게 설정
Dr-KoKo Aug 29, 2024
b975470
[refactor] build.gradle 정리
Dr-KoKo Aug 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
38 changes: 21 additions & 17 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,34 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.3.3'
implementation 'org.redisson:redisson-spring-boot-starter:3.35.0'

// https://mvnrepository.com/artifact/com.github.codemonstur/embedded-redis
implementation 'com.github.codemonstur:embedded-redis:1.4.3'

// jpa
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// QueryDsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// redisson
implementation 'org.redisson:redisson-spring-boot-starter:3.35.0'
// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// jdbc driver
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// embedded redis
implementation 'com.github.codemonstur:embedded-redis:1.4.3'
// actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
}

tasks.named('test') {
Expand All @@ -72,4 +76,4 @@ compileJava.dependsOn clean
// java source set 에 querydsl QClass 위치 추가
sourceSets {
main.java.srcDirs += "$projectDir/build/generated"
}
}
11 changes: 8 additions & 3 deletions src/main/java/camp/woowak/lab/cart/domain/Cart.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

@Getter
public class Cart {
private Long id;
private final String customerId;
private final List<CartItem> cartItems;

Expand All @@ -29,12 +30,16 @@ public Cart(String customerId) {
}

/**
* 해당 Domain을 사용하는 같은 패키지내의 클래스, 혹은 자식 클래스는 List를 커스텀할 수 있습니다.
*
* @param customerId 장바구니 소유주의 ID값입니다.
* @param cartItems 장바구니에 사용될 List입니다.
*/
protected Cart(String customerId, List<CartItem> cartItems) {
public Cart(String customerId, List<CartItem> cartItems) {
this.customerId = customerId;
this.cartItems = cartItems;
}

public Cart(Long id, String customerId, List<CartItem> cartItems) {
this.id = id;
this.customerId = customerId;
this.cartItems = cartItems;
}
Expand Down
21 changes: 10 additions & 11 deletions src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import java.util.Objects;

import lombok.Getter;

@Getter
public class CartItem {
private Long id;
private final Long menuId;
private final Long storeId;
private final int amount;
Expand All @@ -13,20 +17,15 @@ public CartItem(Long menuId, Long storeId, Integer amount) {
this.amount = amount;
}

public Long getMenuId() {
return menuId;
}

public Long getStoreId() {
return storeId;
}

public int getAmount() {
return amount;
public CartItem(Long id, Long menuId, Long storeId, int amount) {
this.id = id;
this.menuId = menuId;
this.storeId = storeId;
this.amount = amount;
}

public CartItem add(Integer increment) {
return new CartItem(menuId, storeId, amount + increment);
return new CartItem(id, menuId, storeId, amount + increment);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
public enum CartErrorCode implements ErrorCode {
MENU_NOT_FOUND(HttpStatus.NOT_FOUND, "ca_1_1", "해당 메뉴가 존재하지 않습니다."),
OTHER_STORE_MENU(HttpStatus.BAD_REQUEST, "ca_1_2", "다른 매장의 메뉴는 등록할 수 없습니다."),
STORE_NOT_OPEN(HttpStatus.BAD_REQUEST, "ca_1_3", "주문 가능한 시간이 아닙니다.");
STORE_NOT_OPEN(HttpStatus.BAD_REQUEST, "ca_1_3", "주문 가능한 시간이 아닙니다."),
NOT_FOUND(HttpStatus.BAD_REQUEST, "ca_2_1", "카트에 담긴 상품이 없습니다.");

private final int status;
private final String errorCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package camp.woowak.lab.cart.exception;

import camp.woowak.lab.common.exception.BadRequestException;

public class NotFoundCartException extends BadRequestException {
public NotFoundCartException(String message) {
super(CartErrorCode.NOT_FOUND, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package camp.woowak.lab.cart.persistence.jpa.entity;

import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

import camp.woowak.lab.cart.domain.Cart;
import camp.woowak.lab.cart.domain.vo.CartItem;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CartEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private UUID customerId;
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CartItemEntity> cartItems;

public Cart toDomain() {
List<CartItem> cartItems = this.cartItems.stream().map(CartItemEntity::toDomain).collect(Collectors.toList());
return new Cart(id, customerId.toString(), cartItems);
}

public static CartEntity fromDomain(Cart cart) {
CartEntity entity = new CartEntity();
entity.id = cart.getId();
entity.customerId = UUID.fromString(cart.getCustomerId());
entity.cartItems = cart.getCartItems()
.stream()
.map((cartItem) -> CartItemEntity.fromDomain(entity, cartItem))
.collect(Collectors.toList());
return entity;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package camp.woowak.lab.cart.persistence.jpa.entity;

import camp.woowak.lab.cart.domain.vo.CartItem;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;

@Entity
public class CartItemEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long menuId;
private Long storeId;
private int amount;

@ManyToOne
private CartEntity cart;

public CartItem toDomain() {
return new CartItem(id, menuId, storeId, amount);
}

public static CartItemEntity fromDomain(CartEntity cartEntity, CartItem cartItem) {
CartItemEntity entity = new CartItemEntity();
entity.id = cartItem.getId();
entity.menuId = cartItem.getMenuId();
entity.storeId = cartItem.getStoreId();
entity.amount = cartItem.getAmount();
entity.cart = cartEntity;
return entity;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package camp.woowak.lab.cart.persistence.jpa.repository;

import java.util.Optional;
import java.util.UUID;

import org.springframework.data.jpa.repository.JpaRepository;

import camp.woowak.lab.cart.persistence.jpa.entity.CartEntity;

public interface CartEntityRepository extends JpaRepository<CartEntity, Long> {
Optional<CartEntity> findByCustomerId(UUID customerId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package camp.woowak.lab.cart.persistence.jpa.repository;

import java.util.Optional;
import java.util.UUID;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Repository;

import camp.woowak.lab.cart.domain.Cart;
import camp.woowak.lab.cart.persistence.jpa.entity.CartEntity;
import camp.woowak.lab.cart.repository.CartRepository;

@Repository
@ConditionalOnProperty(name="cart.repository", havingValue = "jpa")
public class JpaCartRepository implements CartRepository {
private final CartEntityRepository entityRepository;

public JpaCartRepository(CartEntityRepository entityRepository) {
this.entityRepository = entityRepository;
}

@Override
public Optional<Cart> findByCustomerId(String customerId) {
Optional<CartEntity> entity = entityRepository.findByCustomerId(UUID.fromString(customerId));
if (entity.isEmpty()) {
return Optional.empty();
}
return entity.map(CartEntity::toDomain);
}

@Override
public Cart save(Cart cart) {
CartEntity entity = CartEntity.fromDomain(cart);
CartEntity save = entityRepository.save(entity);
return save.toDomain();
}

@Override
public void delete(Cart cart) {
CartEntity cartEntity = CartEntity.fromDomain(cart);
entityRepository.delete(cartEntity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package camp.woowak.lab.cart.persistence.redis.entity;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import camp.woowak.lab.cart.domain.Cart;
import lombok.Getter;

@RedisHash("cart")
@Getter
public class RedisCartEntity implements Serializable {
@Id
private String customerId;

private List<RedisCartItemEntity> cartItems;

@JsonCreator
private RedisCartEntity(@JsonProperty("customerId") String customerId,
@JsonProperty("cartItems") List<RedisCartItemEntity> cartItems) {
this.customerId = customerId;
this.cartItems = cartItems == null ? new ArrayList<>() : cartItems;
}

public static RedisCartEntity fromDomain(Cart cart) {
List<RedisCartItemEntity> list = cart.getCartItems().stream()
.map(RedisCartItemEntity::fromDomain)
.collect(Collectors.toList());
return new RedisCartEntity(cart.getCustomerId(), list);
}

public Cart toDomain() {
return new Cart(customerId, cartItems.stream()
.map(RedisCartItemEntity::toDomain)
.collect(Collectors.toList()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package camp.woowak.lab.cart.persistence.redis.entity;

import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import camp.woowak.lab.cart.domain.vo.CartItem;
import lombok.Getter;

@Getter
public class RedisCartItemEntity implements Serializable {
private Long menuId;
private Long storeId;
private int amount;

@JsonCreator
private RedisCartItemEntity(@JsonProperty("menuId") Long menuId,
@JsonProperty("storeId") Long storeId,
@JsonProperty("amount") int amount) {
this.menuId = menuId;
this.storeId = storeId;
this.amount = amount;
}

protected static RedisCartItemEntity fromDomain(CartItem item) {
return new RedisCartItemEntity(item.getMenuId(), item.getStoreId(), item.getAmount());
}

protected CartItem toDomain() {
return new CartItem(menuId, storeId, amount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package camp.woowak.lab.cart.persistence.redis.repository;

import java.util.Optional;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.repository.CrudRepository;

import camp.woowak.lab.cart.domain.Cart;
import camp.woowak.lab.cart.persistence.redis.entity.RedisCartEntity;
import camp.woowak.lab.cart.repository.CartRepository;

@ConditionalOnProperty(name = "cart.repository", havingValue = "redis")
public interface RedisCartRepository extends CrudRepository<RedisCartEntity, String>, CartRepository {
@Override
default Optional<Cart> findByCustomerId(String customerId) {
return findById(customerId).map(RedisCartEntity::toDomain);
}

@Override
default Cart save(Cart cart) {
RedisCartEntity entity = RedisCartEntity.fromDomain(cart);
return save(entity).toDomain();
}

@Override
default void delete(Cart cart) {
deleteById(cart.getCustomerId());
}
}
Loading