diff --git a/build.gradle b/build.gradle index 4e7b2c84..9592ebc0 100644 --- a/build.gradle +++ b/build.gradle @@ -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') { @@ -72,4 +76,4 @@ compileJava.dependsOn clean // java source set 에 querydsl QClass 위치 추가 sourceSets { main.java.srcDirs += "$projectDir/build/generated" -} \ No newline at end of file +} diff --git a/src/main/java/camp/woowak/lab/cart/domain/Cart.java b/src/main/java/camp/woowak/lab/cart/domain/Cart.java index 8bda0433..8f7f0c4e 100644 --- a/src/main/java/camp/woowak/lab/cart/domain/Cart.java +++ b/src/main/java/camp/woowak/lab/cart/domain/Cart.java @@ -16,6 +16,7 @@ @Getter public class Cart { + private Long id; private final String customerId; private final List cartItems; @@ -29,12 +30,16 @@ public Cart(String customerId) { } /** - * 해당 Domain을 사용하는 같은 패키지내의 클래스, 혹은 자식 클래스는 List를 커스텀할 수 있습니다. - * * @param customerId 장바구니 소유주의 ID값입니다. * @param cartItems 장바구니에 사용될 List입니다. */ - protected Cart(String customerId, List cartItems) { + public Cart(String customerId, List cartItems) { + this.customerId = customerId; + this.cartItems = cartItems; + } + + public Cart(Long id, String customerId, List cartItems) { + this.id = id; this.customerId = customerId; this.cartItems = cartItems; } diff --git a/src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java b/src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java index 2592a33d..5b852e19 100644 --- a/src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java +++ b/src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java @@ -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; @@ -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 diff --git a/src/main/java/camp/woowak/lab/cart/exception/CartErrorCode.java b/src/main/java/camp/woowak/lab/cart/exception/CartErrorCode.java index ca3ed18a..d3e41cc9 100644 --- a/src/main/java/camp/woowak/lab/cart/exception/CartErrorCode.java +++ b/src/main/java/camp/woowak/lab/cart/exception/CartErrorCode.java @@ -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; diff --git a/src/main/java/camp/woowak/lab/cart/exception/NotFoundCartException.java b/src/main/java/camp/woowak/lab/cart/exception/NotFoundCartException.java new file mode 100644 index 00000000..d8dcdebb --- /dev/null +++ b/src/main/java/camp/woowak/lab/cart/exception/NotFoundCartException.java @@ -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); + } +} diff --git a/src/main/java/camp/woowak/lab/cart/persistence/jpa/entity/CartEntity.java b/src/main/java/camp/woowak/lab/cart/persistence/jpa/entity/CartEntity.java new file mode 100644 index 00000000..26e50bc9 --- /dev/null +++ b/src/main/java/camp/woowak/lab/cart/persistence/jpa/entity/CartEntity.java @@ -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 cartItems; + + public Cart toDomain() { + List 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; + } +} diff --git a/src/main/java/camp/woowak/lab/cart/persistence/jpa/entity/CartItemEntity.java b/src/main/java/camp/woowak/lab/cart/persistence/jpa/entity/CartItemEntity.java new file mode 100644 index 00000000..c80d14b2 --- /dev/null +++ b/src/main/java/camp/woowak/lab/cart/persistence/jpa/entity/CartItemEntity.java @@ -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; + } +} diff --git a/src/main/java/camp/woowak/lab/cart/persistence/jpa/repository/CartEntityRepository.java b/src/main/java/camp/woowak/lab/cart/persistence/jpa/repository/CartEntityRepository.java new file mode 100644 index 00000000..ca0177d4 --- /dev/null +++ b/src/main/java/camp/woowak/lab/cart/persistence/jpa/repository/CartEntityRepository.java @@ -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 { + Optional findByCustomerId(UUID customerId); +} diff --git a/src/main/java/camp/woowak/lab/cart/persistence/jpa/repository/JpaCartRepository.java b/src/main/java/camp/woowak/lab/cart/persistence/jpa/repository/JpaCartRepository.java new file mode 100644 index 00000000..d6064374 --- /dev/null +++ b/src/main/java/camp/woowak/lab/cart/persistence/jpa/repository/JpaCartRepository.java @@ -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 findByCustomerId(String customerId) { + Optional 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); + } +} diff --git a/src/main/java/camp/woowak/lab/cart/persistence/redis/entity/RedisCartEntity.java b/src/main/java/camp/woowak/lab/cart/persistence/redis/entity/RedisCartEntity.java new file mode 100644 index 00000000..41fae504 --- /dev/null +++ b/src/main/java/camp/woowak/lab/cart/persistence/redis/entity/RedisCartEntity.java @@ -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 cartItems; + + @JsonCreator + private RedisCartEntity(@JsonProperty("customerId") String customerId, + @JsonProperty("cartItems") List cartItems) { + this.customerId = customerId; + this.cartItems = cartItems == null ? new ArrayList<>() : cartItems; + } + + public static RedisCartEntity fromDomain(Cart cart) { + List 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())); + } +} diff --git a/src/main/java/camp/woowak/lab/cart/persistence/redis/entity/RedisCartItemEntity.java b/src/main/java/camp/woowak/lab/cart/persistence/redis/entity/RedisCartItemEntity.java new file mode 100644 index 00000000..af75fd5c --- /dev/null +++ b/src/main/java/camp/woowak/lab/cart/persistence/redis/entity/RedisCartItemEntity.java @@ -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); + } +} diff --git a/src/main/java/camp/woowak/lab/cart/persistence/redis/repository/RedisCartRepository.java b/src/main/java/camp/woowak/lab/cart/persistence/redis/repository/RedisCartRepository.java new file mode 100644 index 00000000..d53b87be --- /dev/null +++ b/src/main/java/camp/woowak/lab/cart/persistence/redis/repository/RedisCartRepository.java @@ -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, CartRepository { + @Override + default Optional 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()); + } +} diff --git a/src/main/java/camp/woowak/lab/cart/repository/InMemoryCartRepository.java b/src/main/java/camp/woowak/lab/cart/repository/InMemoryCartRepository.java index 8934ba70..6b8043fe 100644 --- a/src/main/java/camp/woowak/lab/cart/repository/InMemoryCartRepository.java +++ b/src/main/java/camp/woowak/lab/cart/repository/InMemoryCartRepository.java @@ -4,6 +4,8 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Repository; import camp.woowak.lab.cart.domain.Cart; @@ -12,6 +14,7 @@ * TODO : Null체크에 대한 Validation */ @Repository +@ConditionalOnProperty(name="cart.repository",havingValue = "in_memory") public class InMemoryCartRepository implements CartRepository { private static final Map cartMap = new ConcurrentHashMap<>(); diff --git a/src/main/java/camp/woowak/lab/cart/service/CartService.java b/src/main/java/camp/woowak/lab/cart/service/CartService.java index 97c9ee59..8187cdd6 100644 --- a/src/main/java/camp/woowak/lab/cart/service/CartService.java +++ b/src/main/java/camp/woowak/lab/cart/service/CartService.java @@ -3,6 +3,7 @@ import java.util.List; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import camp.woowak.lab.cart.domain.Cart; import camp.woowak.lab.cart.domain.vo.CartItem; @@ -17,6 +18,7 @@ @Slf4j @Service +@Transactional public class CartService { private final CartRepository cartRepository; private final MenuRepository menuRepository; diff --git a/src/main/java/camp/woowak/lab/infra/aop/DistributedLock.java b/src/main/java/camp/woowak/lab/infra/aop/DistributedLock.java index 329f9401..f70455d5 100644 --- a/src/main/java/camp/woowak/lab/infra/aop/DistributedLock.java +++ b/src/main/java/camp/woowak/lab/infra/aop/DistributedLock.java @@ -9,7 +9,6 @@ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DistributedLock { - /** * @return 분산락의 키 이름 */ @@ -30,4 +29,13 @@ */ long leaseTime() default 5L; + /** + * @return 분산락을 획득하지 못한 스레드들이 던져야하는 exception + */ + Class throwable() default IllegalStateException.class; + + /** + * @return 분산락을 획득하지 못했을 때의 메세지 + */ + String exceptionMessage() default "Unable to acquire lock"; } diff --git a/src/main/java/camp/woowak/lab/infra/aop/DistributedLockAop.java b/src/main/java/camp/woowak/lab/infra/aop/DistributedLockAop.java index abc790b0..bf201cf1 100644 --- a/src/main/java/camp/woowak/lab/infra/aop/DistributedLockAop.java +++ b/src/main/java/camp/woowak/lab/infra/aop/DistributedLockAop.java @@ -1,5 +1,6 @@ package camp.woowak.lab.infra.aop; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; import org.aspectj.lang.ProceedingJoinPoint; @@ -18,7 +19,6 @@ @RequiredArgsConstructor @Slf4j public class DistributedLockAop { - private static final String REDISSON_LOCK_PREFIX = "LOCK:"; private final RedissonClient redissonClient; private final AopForTransaction aopForTransaction; @@ -39,7 +39,13 @@ public Object around(ProceedingJoinPoint joinPoint) throws Throwable { distributedLock.timeUnit()); if (!locked) { log.warn("Failed to acquire lock for method {} with key {}", method.getName(), key); - throw new IllegalStateException("Unable to acquire lock"); + + Class throwable = distributedLock.throwable(); + String exceptionMessage = distributedLock.exceptionMessage(); + Constructor constructor = throwable.getConstructor(String.class); + RuntimeException exceptions = constructor.newInstance(exceptionMessage); + + throw exceptions; } log.info("Acquired lock for method {} with key {}", method.getName(), key); return aopForTransaction.proceed(joinPoint); diff --git a/src/main/java/camp/woowak/lab/infra/config/RedisConfiguration.java b/src/main/java/camp/woowak/lab/infra/config/RedisConfiguration.java index 3fb63778..3583f9e2 100644 --- a/src/main/java/camp/woowak/lab/infra/config/RedisConfiguration.java +++ b/src/main/java/camp/woowak/lab/infra/config/RedisConfiguration.java @@ -3,12 +3,21 @@ import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; +import org.redisson.spring.data.connection.RedissonConnectionFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfiguration { + private static final Logger log = LoggerFactory.getLogger(RedisConfiguration.class); + private static final String REDISSON_URL_PREFIX = "redis://"; @Value("${spring.data.redis.host}") private String redisHost; @@ -18,10 +27,27 @@ public class RedisConfiguration { @Bean public RedissonClient redissonClient() { + log.info("Creating redisson config client start"); Config config = new Config(); config.useSingleServer() .setAddress(REDISSON_URL_PREFIX + redisHost + ":" + redisPort); - return Redisson.create(config); + RedissonClient redissonClient = Redisson.create(config); + log.info("Creating redisson config client end"); + return redissonClient; + } + + @Bean + public RedisConnectionFactory redisConnectionFactory(RedissonClient redissonClient) { + return new RedissonConnectionFactory(redissonClient); } + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + return template; + } } diff --git a/src/main/java/camp/woowak/lab/order/exception/DuplicatedOrderException.java b/src/main/java/camp/woowak/lab/order/exception/DuplicatedOrderException.java new file mode 100644 index 00000000..32216bad --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/exception/DuplicatedOrderException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.order.exception; + +import camp.woowak.lab.common.exception.ConflictException; + +public class DuplicatedOrderException extends ConflictException { + public DuplicatedOrderException(String message) { + super(OrderErrorCode.DUPLICATED_ORDER, message); + } +} diff --git a/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java b/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java index 4b9ef16a..1337d07c 100644 --- a/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java +++ b/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java @@ -9,7 +9,8 @@ public enum OrderErrorCode implements ErrorCode { MULTI_STORE_ORDER(HttpStatus.BAD_REQUEST, "o_1_1", "동시에 하나의 가게에 대한 메뉴만 주문할 수 있습니다."), NOT_ENOUGH_BALANCE(HttpStatus.BAD_REQUEST, "o_1_2", "잔고가 부족합니다."), NOT_FOUND_MENU(HttpStatus.BAD_REQUEST, "o_1_4", "없는 메뉴입니다."), - MIN_ORDER_PRICE(HttpStatus.BAD_REQUEST, "o_1_5", "최소 주문금액 이상을 주문해야 합니다."); + MIN_ORDER_PRICE(HttpStatus.BAD_REQUEST, "o_1_5", "최소 주문금액 이상을 주문해야 합니다."), + DUPLICATED_ORDER(HttpStatus.BAD_REQUEST, "o_1_6", "중복 결제 요청입니다."); private final int status; private final String errorCode; diff --git a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java index 08ee841b..eace181f 100644 --- a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java +++ b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java @@ -11,6 +11,7 @@ import camp.woowak.lab.cart.repository.CartRepository; import camp.woowak.lab.customer.domain.Customer; import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.infra.aop.DistributedLock; import camp.woowak.lab.infra.date.DateTimeProvider; import camp.woowak.lab.menu.exception.NotEnoughStockException; import camp.woowak.lab.order.domain.Order; @@ -18,6 +19,7 @@ import camp.woowak.lab.order.domain.SingleStoreOrderValidator; import camp.woowak.lab.order.domain.StockRequester; import camp.woowak.lab.order.domain.WithdrawPointService; +import camp.woowak.lab.order.exception.DuplicatedOrderException; import camp.woowak.lab.order.exception.EmptyCartException; import camp.woowak.lab.order.exception.MinimumOrderPriceNotMetException; import camp.woowak.lab.order.exception.MultiStoreOrderException; @@ -48,19 +50,19 @@ public class OrderCreationService { private final DateTimeProvider dateTimeProvider; /** - * @throws EmptyCartException 카트가 비어 있는 경우 - * @throws NotFoundStoreException 가게가 조회되지 않는 경우 - * @throws MultiStoreOrderException 여러 가게의 메뉴를 주문한 경우 - * @throws NotEnoughStockException 메뉴의 재고가 부족한 경우 - * @throws NotFoundMenuException 주문한 메뉴가 조회되지 않는 경우 + * @throws EmptyCartException 카트가 비어 있는 경우 + * @throws NotFoundStoreException 가게가 조회되지 않는 경우 + * @throws MultiStoreOrderException 여러 가게의 메뉴를 주문한 경우 + * @throws NotEnoughStockException 메뉴의 재고가 부족한 경우 + * @throws NotFoundMenuException 주문한 메뉴가 조회되지 않는 경우 * @throws MinimumOrderPriceNotMetException 가게의 최소 주문금액보다 적은 금액을 주문한 경우 - * @throws NotFoundAccountException 구매자의 계좌가 조회되지 않는 경우 - * @throws InsufficientBalanceException 구매자의 계좌에 잔액이 충분하지 않은 경우 + * @throws NotFoundAccountException 구매자의 계좌가 조회되지 않는 경우 + * @throws InsufficientBalanceException 구매자의 계좌에 잔액이 충분하지 않은 경우 */ + @DistributedLock(key = "#cmd.requesterId()", throwable = DuplicatedOrderException.class, exceptionMessage = "중복된 요청입니다.", waitTime = 0L) public Long create(final OrderCreationCommand cmd) { UUID requesterId = cmd.requesterId(); Customer requester = customerRepository.findByIdOrThrow(requesterId); - Cart cart = cartRepository.findByCustomerId(requesterId.toString()) .orElseThrow(() -> new EmptyCartException("구매자 " + requesterId + "가 비어있는 카트로 주문을 시도했습니다.")); List cartItems = cart.getCartItems(); diff --git a/src/main/java/camp/woowak/lab/web/advice/APIResponseAdvice.java b/src/main/java/camp/woowak/lab/web/advice/APIResponseAdvice.java index c253e586..9965f407 100644 --- a/src/main/java/camp/woowak/lab/web/advice/APIResponseAdvice.java +++ b/src/main/java/camp/woowak/lab/web/advice/APIResponseAdvice.java @@ -1,5 +1,7 @@ package camp.woowak.lab.web.advice; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -21,7 +23,13 @@ @Component @RestControllerAdvice @Slf4j +@EnableConfigurationProperties(WebEndpointProperties.class) public class APIResponseAdvice implements ResponseBodyAdvice { + private final WebEndpointProperties webEndpointProperties; + + public APIResponseAdvice(WebEndpointProperties webEndpointProperties) { + this.webEndpointProperties = webEndpointProperties; + } @Override public boolean supports(MethodParameter returnType, Class> converterType) { @@ -35,6 +43,9 @@ public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response ) { + if (request.getURI().getPath().startsWith(webEndpointProperties.getBasePath())) { + return body; + } HttpStatus status = getHttpStatus(returnType, (ServletServerHttpResponse)response); if (body == null) { return new APIResponse<>(status, "No content"); diff --git a/src/main/java/camp/woowak/lab/web/api/cart/CartApiController.java b/src/main/java/camp/woowak/lab/web/api/cart/CartApiController.java index 97704abe..f4a2e358 100644 --- a/src/main/java/camp/woowak/lab/web/api/cart/CartApiController.java +++ b/src/main/java/camp/woowak/lab/web/api/cart/CartApiController.java @@ -6,16 +6,16 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import camp.woowak.lab.cart.domain.Cart; -import camp.woowak.lab.cart.domain.vo.CartItem; +import camp.woowak.lab.cart.exception.NotFoundCartException; import camp.woowak.lab.cart.service.CartService; import camp.woowak.lab.cart.service.command.AddCartCommand; import camp.woowak.lab.cart.service.command.CartTotalPriceCommand; import camp.woowak.lab.web.authentication.LoginCustomer; import camp.woowak.lab.web.authentication.annotation.AuthenticationPrincipal; +import camp.woowak.lab.web.dao.cart.CartDao; import camp.woowak.lab.web.dto.request.cart.AddCartRequest; +import camp.woowak.lab.web.dto.response.CartResponse; import camp.woowak.lab.web.dto.response.cart.AddCartResponse; -import camp.woowak.lab.web.dto.response.cart.CartResponse; import camp.woowak.lab.web.dto.response.cart.CartTotalPriceResponse; import lombok.extern.slf4j.Slf4j; @@ -24,17 +24,11 @@ @Slf4j public class CartApiController { private final CartService cartService; + private final CartDao cartDao; - public CartApiController(CartService cartService) { + public CartApiController(CartService cartService, CartDao cartDao) { this.cartService = cartService; - } - - @GetMapping - public CartResponse getCart(@AuthenticationPrincipal LoginCustomer customer) { - Cart cart = cartService.getCart(customer.getId().toString()); - long totalPrice = cartService.getTotalPrice(new CartTotalPriceCommand(customer.getId().toString())); - return new CartResponse(cart.getCustomerId(), cart.getCartItems().stream().mapToLong(CartItem::getAmount).sum(), - totalPrice); + this.cartDao = cartDao; } @PostMapping @@ -59,4 +53,12 @@ public CartTotalPriceResponse getCartTotalPrice( return new CartTotalPriceResponse(totalPrice); } + @GetMapping + public CartResponse getMyCart(@AuthenticationPrincipal LoginCustomer loginCustomer) { + CartResponse cartResponse = cartDao.findByCustomerId(loginCustomer.getId()); + if (cartResponse == null) { + throw new NotFoundCartException(loginCustomer.getId() + "의 카트가 조회되지 않았습니다."); + } + return cartResponse; + } } diff --git a/src/main/java/camp/woowak/lab/web/dao/cart/CartDao.java b/src/main/java/camp/woowak/lab/web/dao/cart/CartDao.java new file mode 100644 index 00000000..83915632 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dao/cart/CartDao.java @@ -0,0 +1,12 @@ +package camp.woowak.lab.web.dao.cart; + +import java.util.UUID; + +import camp.woowak.lab.web.dto.response.CartResponse; + +public interface CartDao { + /** + * @throws camp.woowak.lab.cart.exception.NotFoundCartException + */ + CartResponse findByCustomerId(UUID customerId); +} diff --git a/src/main/java/camp/woowak/lab/web/dao/cart/JpaCartDao.java b/src/main/java/camp/woowak/lab/web/dao/cart/JpaCartDao.java new file mode 100644 index 00000000..cce1dc5a --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dao/cart/JpaCartDao.java @@ -0,0 +1,66 @@ +package camp.woowak.lab.web.dao.cart; + +import static camp.woowak.lab.cart.persistence.jpa.entity.QCartEntity.*; +import static camp.woowak.lab.cart.persistence.jpa.entity.QCartItemEntity.*; +import static camp.woowak.lab.menu.domain.QMenu.*; +import static camp.woowak.lab.store.domain.QStore.*; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; + +import com.querydsl.core.Tuple; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import camp.woowak.lab.web.dto.response.CartResponse; + +@Repository +@ConditionalOnProperty(name = "cart.dao", havingValue = "jpa") +public class JpaCartDao implements CartDao { + private final JPAQueryFactory queryFactory; + + public JpaCartDao(JPAQueryFactory queryFactory) { + this.queryFactory = queryFactory; + } + + @Override + public CartResponse findByCustomerId(UUID customerId) { + List results = queryFactory + .select(store.id, store.name, store.minOrderPrice, + menu.id, menu.name, menu.price, cartItemEntity.amount, menu.stockCount) + .from(cartEntity) + .leftJoin(cartEntity.cartItems, cartItemEntity) + .join(menu).on(menu.id.eq(cartItemEntity.menuId)) + .join(store).on(store.id.eq(cartItemEntity.storeId)) + .where(eqCustomerId(customerId)) + .fetch(); + + if (results.isEmpty()) { + return null; // 또는 적절한 처리 + } + + Long storeId = results.get(0).get(store.id); + String storeName = results.get(0).get(store.name); + Integer minOrderPrice = results.get(0).get(store.minOrderPrice); + + List items = results.stream() + .map(tuple -> new CartResponse.CartItemInfo( + tuple.get(menu.id), + tuple.get(menu.name), + tuple.get(menu.price), + tuple.get(cartItemEntity.amount), + tuple.get(menu.stockCount) + )) + .collect(Collectors.toList()); + + return new CartResponse(storeId, storeName, minOrderPrice, items); + } + + private BooleanExpression eqCustomerId(UUID customerId) { + return cartEntity.customerId.eq(customerId); + } +} diff --git a/src/main/java/camp/woowak/lab/web/dao/cart/RedisCartDao.java b/src/main/java/camp/woowak/lab/web/dao/cart/RedisCartDao.java new file mode 100644 index 00000000..74eb55eb --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dao/cart/RedisCartDao.java @@ -0,0 +1,94 @@ +package camp.woowak.lab.web.dao.cart; + +import static camp.woowak.lab.menu.domain.QMenu.*; +import static camp.woowak.lab.store.domain.QStore.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; + +import com.querydsl.core.Tuple; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import camp.woowak.lab.cart.domain.Cart; +import camp.woowak.lab.cart.repository.CartRepository; +import camp.woowak.lab.web.dto.response.CartResponse; + +@Repository +@ConditionalOnProperty(name = "cart.dao", havingValue = "redis") +public class RedisCartDao implements CartDao { + private final CartRepository cartRepository; + private final JPAQueryFactory queryFactory; + + public RedisCartDao(CartRepository cartRepository, + JPAQueryFactory queryFactory) { + this.cartRepository = cartRepository; + this.queryFactory = queryFactory; + } + + @Override + public CartResponse findByCustomerId(UUID customerId) { + Optional optionalCart = cartRepository.findByCustomerId(customerId.toString()); + if (optionalCart.isEmpty()) { + return new CartResponse(); + } + Cart cart = optionalCart.get(); + + if (cart.getCartItems().isEmpty()) { + return new CartResponse(); + } + + Long sId = cart.getCartItems().stream().findAny().get().getStoreId(); + Map menuIdsCount = new HashMap<>(); + Set menuIds = cart.getCartItems().stream() + .map((cartItem) -> { + int amount = cartItem.getAmount(); + menuIdsCount.put(cartItem.getMenuId(), amount); + return cartItem.getMenuId(); + }) + .collect(Collectors.toSet()); + + List results = queryFactory + .select( + store.id, + store.name, + store.minOrderPrice, + menu.id, + menu.name, + menu.price, + menu.stockCount + ) + .from(store) + .join(menu).on(menu.store.id.eq(store.id)) + .where(store.id.eq(sId).and(menu.id.in(menuIds))) + .fetch(); + + if (results.isEmpty()) { + return null; // 또는 적절한 예외 처리 + } + + Tuple firstResult = results.get(0); + Long storeId = firstResult.get(store.id); + String storeName = firstResult.get(store.name); + Integer minOrderPrice = firstResult.get(store.minOrderPrice); + + List menuList = results.stream() + .map(tuple -> new CartResponse.CartItemInfo( + tuple.get(menu.id), + tuple.get(menu.name), + tuple.get(menu.price), + menuIdsCount.get(tuple.get(menu.id)), // amount 대신 stockCount 사용 + tuple.get(menu.stockCount) + )) + .collect(Collectors.toList()); + + return new CartResponse(storeId, storeName, minOrderPrice, menuList); + } +} diff --git a/src/main/java/camp/woowak/lab/web/dto/response/CartResponse.java b/src/main/java/camp/woowak/lab/web/dto/response/CartResponse.java new file mode 100644 index 00000000..f301a571 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/response/CartResponse.java @@ -0,0 +1,41 @@ +package camp.woowak.lab.web.dto.response; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Getter; + +@Getter +public class CartResponse { + private Long storeId; + private String storeName; + private Integer minOrderPrice; + private List menus = new ArrayList<>(); + + public CartResponse() { + } + + public CartResponse(Long storId, String storeName, Integer minOrderPrice, List menus) { + this.storeId = storId; + this.storeName = storeName; + this.minOrderPrice = minOrderPrice; + this.menus = menus; + } + + @Getter + public static class CartItemInfo { + private Long menuId; + private String menuName; + private Long menuPrice; + private Integer amount; + private Long leftAmount; + + public CartItemInfo(Long menuId, String menuName, Long menuPrice, Integer amount, Long leftAmount) { + this.menuId = menuId; + this.menuName = menuName; + this.menuPrice = menuPrice; + this.amount = amount; + this.leftAmount = leftAmount; + } + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 3f537835..18bc237d 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -27,3 +27,16 @@ spring: host: localhost port: 6379 password: + +cart: + repository: redis + dao: redis + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always diff --git a/src/test/java/camp/woowak/lab/cart/persistence/jpa/repository/JpaCartRepositoryTest.java b/src/test/java/camp/woowak/lab/cart/persistence/jpa/repository/JpaCartRepositoryTest.java new file mode 100644 index 00000000..615240f8 --- /dev/null +++ b/src/test/java/camp/woowak/lab/cart/persistence/jpa/repository/JpaCartRepositoryTest.java @@ -0,0 +1,93 @@ +package camp.woowak.lab.cart.persistence.jpa.repository; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +import camp.woowak.lab.cart.domain.Cart; +import camp.woowak.lab.cart.repository.CartRepository; + +@DataJpaTest +@Transactional +class JpaCartRepositoryTest { + @Autowired + private TransactionTemplate transactionTemplate; + + @Autowired + private CartRepository cartRepository; + @Autowired + private CartEntityRepository cartEntityRepository; + + private UUID fakeCustomerId; + + @TestConfiguration + static class TestContextConfiguration { + @Bean + public CartRepository cartRepository(CartEntityRepository cartEntityRepository) { + return new JpaCartRepository(cartEntityRepository); + } + } + + @BeforeEach + void setUp() { + fakeCustomerId = UUID.randomUUID(); + cartRepository.save(new Cart(fakeCustomerId.toString())); + } + + @Nested + @DisplayName("[findByCustomerId]") + class FindByCustomerIdIs { + @Test + @DisplayName("저장된 CartEntity가 있는 경우") + void returnOptionalWhenCartEntityIsFound() { + Optional findCart = transactionTemplate.execute( + (status) -> cartRepository.findByCustomerId(fakeCustomerId.toString())); + assertThat(findCart.isPresent()).isTrue(); + assertThat(findCart.get().getCustomerId()).isEqualTo(fakeCustomerId.toString()); + } + + @Test + @DisplayName("저장된 CartEntity가 없는 경우") + void returnEmptyOptionalWhenCartEntityIsNotFound() { + cartEntityRepository.deleteAll(); + Optional findCart = transactionTemplate.execute( + (status) -> cartRepository.findByCustomerId(fakeCustomerId.toString())); + assertThat(findCart.isPresent()).isFalse(); + } + } + + @Nested + @DisplayName("[save]") + class SaveIs { + @Test + @DisplayName("[예외] 같은 구매자 id로 카트를 저장하면") + void duplicateCustomerId() { + Assertions.assertThrows(DataIntegrityViolationException.class, + () -> cartRepository.save(new Cart(fakeCustomerId.toString(), List.of()))); + } + + @Test + @DisplayName("[성공] 중복되지 않은 구매자 id로 카트를 저장하면") + void success() { + UUID newFakeCustomerId = UUID.randomUUID(); + Cart save = cartRepository.save(new Cart(newFakeCustomerId.toString())); + + assertThat(save.getCustomerId()).isEqualTo(newFakeCustomerId.toString()); + } + } +} diff --git a/src/test/java/camp/woowak/lab/cart/persistence/redis/repository/RedisCartRepositoryTest.java b/src/test/java/camp/woowak/lab/cart/persistence/redis/repository/RedisCartRepositoryTest.java new file mode 100644 index 00000000..a7b49ef1 --- /dev/null +++ b/src/test/java/camp/woowak/lab/cart/persistence/redis/repository/RedisCartRepositoryTest.java @@ -0,0 +1,129 @@ +package camp.woowak.lab.cart.persistence.redis.repository; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.TestPropertySources; + +import camp.woowak.lab.cart.domain.Cart; +import camp.woowak.lab.cart.domain.vo.CartItem; + +@SpringBootTest +@TestPropertySources({ + @TestPropertySource(properties = "cart.dao=redis"), + @TestPropertySource(properties = "cart.repository=redis") +}) +public class RedisCartRepositoryTest { + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private RedisCartRepository repository; + + private static final String CUSTOMER_ID_EXIST = UUID.randomUUID().toString(); + private static final String CUSTOMER_ID_NOT_EXIST = UUID.randomUUID().toString(); + private Cart cart; + + @BeforeEach + void setUp() { + List cartItemList = List.of(new CartItem(1L, 1L, 1), + new CartItem(2L, 2L, 2), + new CartItem(3L, 3L, 3)); + cart = new Cart(CUSTOMER_ID_EXIST, cartItemList); + repository.save(cart); + } + + @AfterEach + void clear() { + repository.delete(cart); + redisTemplate.execute((RedisConnection connection) -> { + connection.serverCommands().flushAll(); + return null; + }); + } + + @Nested + @DisplayName("findByCustomerId 메서드") + class FindByCustomerIdTest { + @Test + @DisplayName("customerId에 해당하는 Cart가 없으면 Optional(null)을 반환한다.") + void NullReturnWhenCustomerIdNotExists() { + //when + Optional foundCart = repository.findByCustomerId(CUSTOMER_ID_NOT_EXIST); + + //then + assertThat(foundCart).isEmpty(); + } + + @Test + @DisplayName("customerId에 해당하는 Cart가 있으면 해당 Cart를 반환한다.") + void cartReturnWhenCustomerIdExists() { + //when + Optional foundCart = repository.findByCustomerId(CUSTOMER_ID_EXIST); + + //then + assertThat(foundCart).isPresent(); + assertCart(cart, foundCart.get()); + } + } + + @Nested + @DisplayName("save 메서드") + class SaveTest { + @Test + @DisplayName("cart를 저장할 수 있다.") + void saveTest() { + //given + Cart newCart = new Cart(CUSTOMER_ID_NOT_EXIST); + + //when + Cart savedCart = repository.save(newCart); + + //then + assertCart(savedCart, newCart); + Optional foundCart = repository.findByCustomerId(CUSTOMER_ID_NOT_EXIST); + assertThat(foundCart).isPresent(); + Cart foundedCart = foundCart.get(); + assertCart(savedCart, foundedCart); + } + } + + @Nested + @DisplayName("delete 메서드") + class DeleteTest { + @Test + @DisplayName("존재하는 customerId의 cart를 삭제할 수 있다.") + void deleteTest() { + //when + repository.delete(cart); + + //then + Optional foundCart = repository.findByCustomerId(CUSTOMER_ID_EXIST); + assertThat(foundCart).isEmpty(); + } + } + + private void assertCart(Cart expected, Cart actual) { + assertThat(actual.getCustomerId()).isEqualTo(expected.getCustomerId()); + assertThat(actual.getCartItems().size()).isEqualTo(expected.getCartItems().size()); + for (int i = 0; i < expected.getCartItems().size(); i++) { + assertThat(actual.getCartItems().get(i).getAmount()).isEqualTo(expected.getCartItems().get(i).getAmount()); + assertThat(actual.getCartItems().get(i).getMenuId()).isEqualTo(expected.getCartItems().get(i).getMenuId()); + assertThat(actual.getCartItems().get(i).getStoreId()).isEqualTo( + expected.getCartItems().get(i).getStoreId()); + } + } +} \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/cart/service/CartServiceConcurrencyTest.java b/src/test/java/camp/woowak/lab/cart/service/CartServiceConcurrencyTest.java new file mode 100644 index 00000000..2cffd330 --- /dev/null +++ b/src/test/java/camp/woowak/lab/cart/service/CartServiceConcurrencyTest.java @@ -0,0 +1,183 @@ +package camp.woowak.lab.cart.service; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.TestPropertySources; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import camp.woowak.lab.cart.repository.CartRepository; +import camp.woowak.lab.cart.service.command.AddCartCommand; +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.infra.cache.FakeMenuStockCacheService; +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.menu.domain.MenuCategory; +import camp.woowak.lab.menu.repository.MenuCategoryRepository; +import camp.woowak.lab.menu.repository.MenuRepository; +import camp.woowak.lab.order.domain.Order; +import camp.woowak.lab.order.exception.DuplicatedOrderException; +import camp.woowak.lab.order.repository.OrderRepository; +import camp.woowak.lab.order.service.OrderCreationService; +import camp.woowak.lab.order.service.command.OrderCreationCommand; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.store.repository.StoreCategoryRepository; +import camp.woowak.lab.store.repository.StoreRepository; +import camp.woowak.lab.vendor.repository.VendorRepository; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.dao.store.StoreDummiesFixture; + +@SpringBootTest +@TestPropertySources({ + @TestPropertySource(properties = "cart.dao=redis"), + @TestPropertySource(properties = "cart.repository=redis") +}) +@Disabled +public class CartServiceConcurrencyTest extends StoreDummiesFixture { + private final Logger log = LoggerFactory.getLogger(CartServiceConcurrencyTest.class); + + @Autowired + public CartServiceConcurrencyTest(PayAccountRepository payAccountRepository, + StoreRepository storeRepository, + StoreCategoryRepository storeCategoryRepository, + VendorRepository vendorRepository, + OrderRepository orderRepository, + CustomerRepository customerRepository, + MenuRepository menuRepository, + MenuCategoryRepository menuCategoryRepository, + CartRepository cartRepository, + CartService cartService, + OrderCreationService orderCreationService) { + super(storeRepository, storeCategoryRepository, vendorRepository, payAccountRepository, orderRepository, + customerRepository, menuRepository, menuCategoryRepository, new FakeMenuStockCacheService(), + new NoOpPasswordEncoder()); + this.cartService = cartService; + this.cartRepository = cartRepository; + this.orderCreationService = orderCreationService; + this.payAccountRepository = payAccountRepository; + } + + private OrderCreationService orderCreationService; + private final CartService cartService; + private final CartRepository cartRepository; + private final PayAccountRepository payAccountRepository; + + private PayAccount payAccount; + private Customer customer; + private Store store; + private MenuCategory menuCategory; + private List menus; + + @BeforeEach + void setUpDummies() { + payAccount = createPayAccount(1000000L); + customer = createDummyCustomer(payAccount); + store = createDummyStores(1).get(0); + menuCategory = createDummyMenuCategories(store, 1).get(0); + menus = createDummyMenus(store, menuCategory, 5); + } + + @Test + void test() throws InterruptedException { + //given + // 검증을 위해 최초 값 확인 + long originBalance = payAccount.getBalance(); // 현재 구매자의 잔액 확인 + Map originStocks = new HashMap<>(); // 현재 메뉴의 재고 확인 + + // 메뉴를 카트에 저장 + menus.stream() + .forEach(menu -> { + originStocks.put(menu.getId(), menu.getStockCount()); + cartService.addMenu(new AddCartCommand(customer.getId().toString(), menu.getId())); + }); + + // 동시성 테스트 : 주문/결제을 동시에 요청한다. + int numberOfThreads = 10; + ExecutorService ex = Executors.newFixedThreadPool(numberOfThreads); + CountDownLatch latch = new CountDownLatch(numberOfThreads); + List expectedException = Collections.synchronizedList( + new ArrayList<>());//동시성 문제로 터진 Exception을 저장할 리스트 + + //when + //같은 사람이 동시에 numberOfThreads 만큼의 요청을 보내는 상황 + long totalStartTime = System.currentTimeMillis(); + for (int i = 0; i < numberOfThreads; i++) { + ex.submit(() -> { + long startTime = System.currentTimeMillis(); + try { + OrderCreationCommand command = new OrderCreationCommand(customer.getId()); + + orderCreationService.create(command); + } catch (Exception e) { + expectedException.add(e); + } finally { + latch.countDown(); + long endTime = System.currentTimeMillis(); + log.info("순번: {}, 소요 시간: {}ms", numberOfThreads - latch.getCount(), endTime - startTime); + } + }); + } + + latch.await(); + long totalEndTime = System.currentTimeMillis(); + log.info("{}개 쓰레드의 총 소요시간 : {} ms", numberOfThreads, (totalEndTime - totalStartTime)); + + //then + /** + * 1. 주문이 단 한개만 생긴다. 이 주문은 카트에 담긴 메뉴들의 가격의 총 합, requester = customer, order items 가 50개 + * 2. 사용자의 계좌에서 메뉴의 총 가격이 단 한번만 빠져나갔는지? + * 3. 재고가 1~50까지 1개씩만 차감됐는지? + */ + verify(numberOfThreads, originBalance, originStocks, expectedException); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void verify(int numberOfThreads, long originBalance, Map originStocks, + List expectedException) { + List all = orderRepository.findAll(); + + long totalPrice = menus.stream() + .map(Menu::getPrice) + .reduce(0L, Long::sum); + + PayAccount updated = payAccountRepository.findById(payAccount.getId()).get(); + assertAll("verify values", + // 첫번째 주문 빼고는 모두 exception을 발생시켜야함 + () -> assertThat(expectedException.size()).isEqualTo(numberOfThreads - 1), + () -> expectedException + .forEach( + (exception) -> assertThat(exception).isExactlyInstanceOf(DuplicatedOrderException.class)), + //주문은 단 한번만 저장되어야 한다. + () -> assertThat(all.size()).isEqualTo(1L), + //저장된 order의 requester는 주문한 customer다., + () -> assertThat(all.get(0).getRequester().getId()).isEqualTo(customer.getId()), + //주문 이후 Customer의 잔고는 단 한번 차감되어야한다. + () -> assertThat(updated.getBalance()).isEqualTo(originBalance - totalPrice), + //사용자가 주문했던 메뉴들의 재고수는 단 한번 차감되어야한다. + () -> menuRepository.findAll() + .forEach(menu -> { + assertThat(menu.getStockCount()).isEqualTo(originStocks.get(menu.getId()) - 1); + }) + ); + } +} diff --git a/src/test/java/camp/woowak/lab/web/advice/APIResponseAdviceTest.java b/src/test/java/camp/woowak/lab/web/advice/APIResponseAdviceTest.java index d59b3625..c7f16d65 100644 --- a/src/test/java/camp/woowak/lab/web/advice/APIResponseAdviceTest.java +++ b/src/test/java/camp/woowak/lab/web/advice/APIResponseAdviceTest.java @@ -3,6 +3,9 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; +import java.net.URI; +import java.net.URISyntaxException; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -10,6 +13,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -42,6 +46,9 @@ class APIResponseAdviceTest { @Mock private HttpServletResponse servletResponse; + @Mock + private WebEndpointProperties webEndpointProperties; + @Nested @DisplayName("supports 메서드는") class Supports { @@ -95,8 +102,10 @@ void supports_ShouldReturnFalse_WhenReturnTypeIsProblemDetails() { class BeforeBodyWrite { @Test @DisplayName("body가 null일 때 'No content' 메시지를 포함한 APIResponse를 반환해야 한다") - void beforeBodyWrite_ShouldReturnAPIResponse_WhenBodyIsNull() { + void beforeBodyWrite_ShouldReturnAPIResponse_WhenBodyIsNull() throws URISyntaxException { // Given + given(webEndpointProperties.getBasePath()).willReturn("/actuator"); + given(request.getURI()).willReturn(new URI("/api")); given(servletResponse.getStatus()).willReturn(HttpStatus.OK.value()); // When @@ -113,9 +122,11 @@ void beforeBodyWrite_ShouldReturnAPIResponse_WhenBodyIsNull() { @Test @DisplayName("body가 APIResponse가 아닐 때 해당 body를 포함한 새로운 APIResponse를 반환해야 한다") - void beforeBodyWrite_ShouldReturnAPIResponse_WhenBodyIsNotAPIResponse() { + void beforeBodyWrite_ShouldReturnAPIResponse_WhenBodyIsNotAPIResponse() throws URISyntaxException { // Given String body = "Test Body"; + given(webEndpointProperties.getBasePath()).willReturn("/actuator"); + given(request.getURI()).willReturn(new URI("/api")); given(servletResponse.getStatus()).willReturn(HttpStatus.OK.value()); // When @@ -132,10 +143,12 @@ void beforeBodyWrite_ShouldReturnAPIResponse_WhenBodyIsNotAPIResponse() { @Test @DisplayName("beforeBodyWrite 메서드는 ResponseStatus 어노테이션이 있을 때 해당 상태를 사용해야 한다") - void beforeBodyWrite_ShouldUseResponseStatusAnnotation_WhenPresent() { + void beforeBodyWrite_ShouldUseResponseStatusAnnotation_WhenPresent() throws URISyntaxException { // Given String body = "Test Body"; ResponseStatus responseStatus = mock(ResponseStatus.class); + given(webEndpointProperties.getBasePath()).willReturn("/actuator"); + given(request.getURI()).willReturn(new URI("/api")); given(methodParameter.hasMethodAnnotation(ResponseStatus.class)).willReturn(true); given(methodParameter.getMethodAnnotation(ResponseStatus.class)).willReturn(responseStatus); given(responseStatus.value()).willReturn(HttpStatus.CREATED); @@ -153,9 +166,11 @@ void beforeBodyWrite_ShouldUseResponseStatusAnnotation_WhenPresent() { @Test @DisplayName("beforeBodyWrite 메서드는 ResponseStatus 어노테이션이 없을 때 응답의 상태 코드를 사용해야 한다") - void beforeBodyWrite_ShouldUseResponseStatus_WhenNoAnnotationPresent() { + void beforeBodyWrite_ShouldUseResponseStatus_WhenNoAnnotationPresent() throws URISyntaxException { // Given String body = "Test Body"; + given(webEndpointProperties.getBasePath()).willReturn("/actuator"); + given(request.getURI()).willReturn(new URI("/api")); given(methodParameter.hasMethodAnnotation(ResponseStatus.class)).willReturn(false); given(servletResponse.getStatus()).willReturn(HttpStatus.OK.value()); @@ -172,4 +187,4 @@ void beforeBodyWrite_ShouldUseResponseStatus_WhenNoAnnotationPresent() { } } -} \ No newline at end of file +} diff --git a/src/test/java/camp/woowak/lab/web/api/store/StoreApiControllerWithDaoTest.java b/src/test/java/camp/woowak/lab/web/api/store/StoreApiControllerWithDaoTest.java index 3803ac2c..0d2d582f 100644 --- a/src/test/java/camp/woowak/lab/web/api/store/StoreApiControllerWithDaoTest.java +++ b/src/test/java/camp/woowak/lab/web/api/store/StoreApiControllerWithDaoTest.java @@ -25,6 +25,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.infra.cache.FakeMenuStockCacheService; +import camp.woowak.lab.menu.repository.MenuCategoryRepository; import camp.woowak.lab.menu.repository.MenuRepository; import camp.woowak.lab.order.domain.Order; import camp.woowak.lab.order.repository.OrderRepository; @@ -33,6 +35,7 @@ import camp.woowak.lab.store.repository.StoreCategoryRepository; import camp.woowak.lab.store.repository.StoreRepository; import camp.woowak.lab.vendor.repository.VendorRepository; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; import camp.woowak.lab.web.dao.store.StoreDummiesFixture; import camp.woowak.lab.web.dto.request.store.StoreFilterBy; import camp.woowak.lab.web.dto.request.store.StoreInfoListRequestConst; @@ -62,9 +65,10 @@ public StoreApiControllerWithDaoTest(PayAccountRepository payAccountRepository, OrderRepository orderRepository, CustomerRepository customerRepository, MenuRepository menuRepository, + MenuCategoryRepository menuCategoryRepository, ObjectMapper objectMapper, MockMvc mvc) { super(storeRepository, storeCategoryRepository, vendorRepository, payAccountRepository, orderRepository, - customerRepository, menuRepository); + customerRepository, menuRepository, menuCategoryRepository, new FakeMenuStockCacheService(), new NoOpPasswordEncoder()); this.menuRepository = menuRepository; this.objectMapper = objectMapper; this.mvc = mvc; diff --git a/src/test/java/camp/woowak/lab/web/dao/cart/RedisCartDaoTest.java b/src/test/java/camp/woowak/lab/web/dao/cart/RedisCartDaoTest.java new file mode 100644 index 00000000..79deb6bc --- /dev/null +++ b/src/test/java/camp/woowak/lab/web/dao/cart/RedisCartDaoTest.java @@ -0,0 +1,92 @@ +package camp.woowak.lab.web.dao.cart; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.TestPropertySources; +import org.springframework.transaction.annotation.Transactional; + +import camp.woowak.lab.cart.domain.Cart; +import camp.woowak.lab.cart.repository.CartRepository; +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.infra.cache.FakeMenuStockCacheService; +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.menu.domain.MenuCategory; +import camp.woowak.lab.menu.repository.MenuCategoryRepository; +import camp.woowak.lab.menu.repository.MenuRepository; +import camp.woowak.lab.order.repository.OrderRepository; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.store.repository.StoreCategoryRepository; +import camp.woowak.lab.store.repository.StoreRepository; +import camp.woowak.lab.vendor.repository.VendorRepository; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.dao.store.StoreDummiesFixture; +import camp.woowak.lab.web.dto.response.CartResponse; + +@SpringBootTest +@Transactional +@TestPropertySources({ + @TestPropertySource(properties = "cart.dao=redis"), + @TestPropertySource(properties = "cart.repository=redis") +}) +class RedisCartDaoTest extends StoreDummiesFixture { + private final CartRepository cartRepository; + @Autowired + private CartDao cartDao; + + @Autowired + public RedisCartDaoTest(PayAccountRepository payAccountRepository, StoreRepository storeRepository, + StoreCategoryRepository storeCategoryRepository, + VendorRepository vendorRepository, + OrderRepository orderRepository, + CustomerRepository customerRepository, + MenuRepository menuRepository, + MenuCategoryRepository menuCategoryRepository, + CartRepository cartRepository) { + super(storeRepository, storeCategoryRepository, vendorRepository, payAccountRepository, orderRepository, + customerRepository, menuRepository, menuCategoryRepository, new FakeMenuStockCacheService(), + new NoOpPasswordEncoder()); + this.cartRepository = cartRepository; + } + + private Customer customer; + private Store store; + private MenuCategory menuCategory; + private List menus; + + @BeforeEach + void setUp() { + customer = createDummyCustomers(1).get(0); + store = createDummyStores(1).get(0); + menuCategory = createDummyMenuCategories(store, 1).get(0); + menus = createDummyMenus(store, menuCategory, 5); + } + + @Test + @DisplayName("findByCustomerId 메서드는 장바구니에 담긴 메뉴의 아이템을 가져온다.") + void findByCustomerIdTest() { + //given + Cart cart = new Cart(customer.getId().toString()); + cartRepository.save(cart); + menus.stream() + .forEach(cart::addMenu); + Cart save = cartRepository.save(cart); + + //when + CartResponse response = cartDao.findByCustomerId(customer.getId()); + + //then + assertThat(response.getStoreId()).isEqualTo(store.getId()); + assertThat(response.getStoreName()).isEqualTo(store.getName()); + assertThat(response.getMinOrderPrice()).isEqualTo(store.getMinOrderPrice()); + } +} \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/web/dao/store/StoreDaoTest.java b/src/test/java/camp/woowak/lab/web/dao/store/StoreDaoTest.java index 8cbb5a17..aa667165 100644 --- a/src/test/java/camp/woowak/lab/web/dao/store/StoreDaoTest.java +++ b/src/test/java/camp/woowak/lab/web/dao/store/StoreDaoTest.java @@ -16,6 +16,8 @@ import org.springframework.transaction.annotation.Transactional; import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.infra.cache.FakeMenuStockCacheService; +import camp.woowak.lab.menu.repository.MenuCategoryRepository; import camp.woowak.lab.menu.repository.MenuRepository; import camp.woowak.lab.order.domain.Order; import camp.woowak.lab.order.repository.OrderRepository; @@ -24,6 +26,7 @@ import camp.woowak.lab.store.repository.StoreCategoryRepository; import camp.woowak.lab.store.repository.StoreRepository; import camp.woowak.lab.vendor.repository.VendorRepository; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; import camp.woowak.lab.web.dto.request.store.StoreFilterBy; import camp.woowak.lab.web.dto.request.store.StoreInfoListRequest; import camp.woowak.lab.web.dto.request.store.StoreInfoListRequestConst; @@ -39,14 +42,16 @@ class StoreDaoTest extends StoreDummiesFixture { private List dummies; @Autowired - public StoreDaoTest(StoreDao storeDao, StoreRepository storeRepository, + public StoreDaoTest(StoreDao storeDao, PayAccountRepository payAccountRepository, StoreRepository storeRepository, StoreCategoryRepository storeCategoryRepository, - VendorRepository vendorRepository, PayAccountRepository payAccountRepository, + VendorRepository vendorRepository, OrderRepository orderRepository, CustomerRepository customerRepository, - MenuRepository menuRepository) { + MenuRepository menuRepository, + MenuCategoryRepository menuCategoryRepository) { super(storeRepository, storeCategoryRepository, vendorRepository, payAccountRepository, orderRepository, - customerRepository, menuRepository); + customerRepository, menuRepository, menuCategoryRepository, new FakeMenuStockCacheService(), + new NoOpPasswordEncoder()); this.storeDao = storeDao; this.dummyCount = 105; } diff --git a/src/test/java/camp/woowak/lab/web/dao/store/StoreDummiesFixture.java b/src/test/java/camp/woowak/lab/web/dao/store/StoreDummiesFixture.java index 4db2b919..798223ad 100644 --- a/src/test/java/camp/woowak/lab/web/dao/store/StoreDummiesFixture.java +++ b/src/test/java/camp/woowak/lab/web/dao/store/StoreDummiesFixture.java @@ -9,8 +9,10 @@ import camp.woowak.lab.cart.domain.vo.CartItem; import camp.woowak.lab.customer.domain.Customer; import camp.woowak.lab.customer.repository.CustomerRepository; -import camp.woowak.lab.infra.cache.FakeMenuStockCacheService; import camp.woowak.lab.infra.cache.MenuStockCacheService; +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.menu.domain.MenuCategory; +import camp.woowak.lab.menu.repository.MenuCategoryRepository; import camp.woowak.lab.menu.repository.MenuRepository; import camp.woowak.lab.order.domain.Order; import camp.woowak.lab.order.domain.PriceChecker; @@ -28,40 +30,54 @@ import camp.woowak.lab.store.repository.StoreRepository; import camp.woowak.lab.vendor.domain.Vendor; import camp.woowak.lab.vendor.repository.VendorRepository; -import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; import camp.woowak.lab.web.authentication.PasswordEncoder; public abstract class StoreDummiesFixture { protected final StoreRepository storeRepository; protected final StoreCategoryRepository storeCategoryRepository; - protected final MenuRepository menuRepository; protected final VendorRepository vendorRepository; protected final PayAccountRepository payAccountRepository; protected final OrderRepository orderRepository; protected final CustomerRepository customerRepository; - protected final PasswordEncoder passwordEncoder; + protected final MenuRepository menuRepository; + protected final MenuCategoryRepository menuCategoryRepository; protected final MenuStockCacheService menuStockCacheService; + protected final PasswordEncoder passwordEncoder; - public StoreDummiesFixture(StoreRepository storeRepository, StoreCategoryRepository storeCategoryRepository, - VendorRepository vendorRepository, PayAccountRepository payAccountRepository, + public StoreDummiesFixture(StoreRepository storeRepository, + StoreCategoryRepository storeCategoryRepository, + VendorRepository vendorRepository, + PayAccountRepository payAccountRepository, OrderRepository orderRepository, - CustomerRepository customerRepository, MenuRepository menuRepository) { + CustomerRepository customerRepository, + MenuRepository menuRepository, + MenuCategoryRepository menuCategoryRepository, + MenuStockCacheService menuStockCacheService, + PasswordEncoder passwordEncoder) { this.storeRepository = storeRepository; this.storeCategoryRepository = storeCategoryRepository; this.vendorRepository = vendorRepository; this.payAccountRepository = payAccountRepository; this.orderRepository = orderRepository; this.customerRepository = customerRepository; - this.passwordEncoder = new NoOpPasswordEncoder(); this.menuRepository = menuRepository; - this.menuStockCacheService = new FakeMenuStockCacheService(); + this.menuCategoryRepository = menuCategoryRepository; + this.menuStockCacheService = menuStockCacheService; + this.passwordEncoder = passwordEncoder; + } + + protected PayAccount createPayAccount(long balance) { + PayAccount payAccount = new PayAccount(); + payAccount.charge(balance); + return payAccountRepository.saveAndFlush(payAccount); } protected List createDummyCustomers(int numberOfCustomers) { List customers = new ArrayList<>(numberOfCustomers); for (int i = 0; i < numberOfCustomers; i++) { PayAccount payAccount = new PayAccount(); - payAccountRepository.save(payAccount); + payAccount.charge(1000000L); + payAccountRepository.saveAndFlush(payAccount); Customer customer = new Customer("customer " + i, "cemail" + i + "@gmail.com", "password1234!", "010-1234-5678", payAccount, passwordEncoder); customers.add(customer); @@ -70,6 +86,12 @@ protected List createDummyCustomers(int numberOfCustomers) { return customerRepository.saveAllAndFlush(customers); } + protected Customer createDummyCustomer(PayAccount payAccount) { + Customer customer = new Customer("customer ", "cemail@gmail.com", "password1234!", + "010-1234-5678", payAccount, passwordEncoder); + return customerRepository.saveAndFlush(customer); + } + protected List createOrdersWithRandomCount(List store) { List dummyCustomers = createDummyCustomers(10); List orders = new ArrayList<>(store.size()); @@ -103,8 +125,8 @@ protected List createDummyStores(int numberOfStores) { String address = StoreAddress.DEFAULT_DISTRICT; String phoneNumber = "123-456-789" + (i % 10); Integer minOrderPrice = 5000 + (random.nextInt(10000)) / 1000 * 1000; - LocalDateTime startTime = LocalDateTime.now().plusHours(random.nextInt(10)).withSecond(0).withNano(0); - LocalDateTime endTime = startTime.plusHours(random.nextInt(20) + 1); + LocalDateTime startTime = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endTime = LocalDateTime.now().withHour(23).withMinute(59).withSecond(0).withNano(0); Store store = new Store(vendor, storeCategory, name, address, phoneNumber, minOrderPrice, startTime, endTime); @@ -115,6 +137,27 @@ protected List createDummyStores(int numberOfStores) { return stores; } + protected List createDummyMenuCategories(Store store, int numberOfMenuCategories) { + List categories = new ArrayList<>(numberOfMenuCategories); + for (int i = 0; i < numberOfMenuCategories; i++) { + MenuCategory menuCategory = new MenuCategory(store, "메뉴 카테고리" + i); + categories.add(menuCategory); + } + return menuCategoryRepository.saveAllAndFlush(categories); + } + + protected List createDummyMenus(Store store, MenuCategory menuCategory, int numberOfMenus) { + List menus = new ArrayList<>(numberOfMenus); + Random random = new Random(); + for (int i = 0; i < numberOfMenus; i++) { + Menu menu = new Menu(store, menuCategory, "메뉴" + i, + Integer.toUnsignedLong(10000 + random.nextInt(1000, 5000)), + Integer.toUnsignedLong(100), "imageUrl" + i); + menus.add(menu); + } + return menuRepository.saveAllAndFlush(menus); + } + protected Vendor createDummyVendor() { PayAccount payAccount = new PayAccount(); payAccountRepository.saveAndFlush(payAccount); diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 3aee28d1..0639c79a 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -16,14 +16,27 @@ spring: jpa: hibernate: ddl-auto: create - properties: hibernate: format_sql: true show_sql: true + use_sql_comments: false data: redis: host: localhost port: 6379 - password: \ No newline at end of file + password: + +cart: + dao: redis + repository: in_memory + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always