Skip to content

Commit

Permalink
Merge pull request #123 from f-lab-edu/feature/#121
Browse files Browse the repository at this point in the history
Feature/#121
  • Loading branch information
pak0426 authored Jul 1, 2024
2 parents d53dc55 + ca7e67d commit 4abb68b
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 69 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'mysql:mysql-connector-java:8.0.33'
implementation 'org.redisson:redisson-spring-boot-starter:3.31.0'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import com.mini.joymall.customer.domain.repository.CustomerAddressRepository;
import com.mini.joymall.order.domain.entity.Order;
import com.mini.joymall.order.domain.entity.OrderItem;
import com.mini.joymall.order.domain.entity.OrderStatus;
import com.mini.joymall.order.domain.repository.OrderRepository;
import com.mini.joymall.order.dto.request.CreateOrderRequest;
import com.mini.joymall.order.dto.response.CreateOrderResponse;
import com.mini.joymall.sale.service.SalesProductFacade;
import com.mini.joymall.sale.service.SalesProductFacadeImpl;
import com.mini.joymall.sale.service.SalesProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -21,7 +22,7 @@
public class OrderService {
private final OrderValidator orderValidator;
private final OrderRepository orderRepository;
private final SalesProductService salesProductService;
private final SalesProductFacade salesProductFacade;
private final CustomerAddressRepository addressRepository;

public CreateOrderResponse createOrder(CreateOrderRequest createOrderRequests) {
Expand All @@ -32,7 +33,7 @@ public CreateOrderResponse createOrder(CreateOrderRequest createOrderRequests) {
.orElseThrow(NoSuchElementException::new);
Order savedOrder = orderRepository.save(Order.ordered(customerAddress.getId(), orderItems));

salesProductService.decreaseStock(orderItems);
salesProductFacade.decreaseStock(orderItems);

return CreateOrderResponse.from(savedOrder, customerAddress);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.mini.joymall.product.dto.response.CategoryChildrenResponse;
import com.mini.joymall.product.dto.CategoryDTO;
import com.mini.joymall.product.service.CategoryService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -13,14 +14,11 @@
import static com.mini.joymall.commons.ApiResponse.*;

@Controller
@RequiredArgsConstructor
public class CategoryController {

private final CategoryService categoryService;

public CategoryController(CategoryService categoryService) {
this.categoryService = categoryService;
}

@GetMapping("/categories")
private ResponseEntity<List<CategoryDTO>> findByDepth(@RequestParam("depth") int depth) {
return OK(categoryService.findByDepth(depth));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.MappedCollection;
import org.springframework.data.relational.core.mapping.Table;

import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;

@TypeAlias("CATEGORY")
@Table("CATEGORY")
@Getter
@ToString
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,17 @@
import com.mini.joymall.product.domain.repository.CategoryRepository;
import com.mini.joymall.product.dto.response.CategoryChildrenResponse;
import com.mini.joymall.product.dto.CategoryDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.NoSuchElementException;

@Service
@RequiredArgsConstructor
public class CategoryService {
private final CategoryRepository categoryRepository;

public CategoryService(CategoryRepository categoryRepository) {
this.categoryRepository = categoryRepository;
}

public List<CategoryDTO> findByDepth(int depth) {
List<Category> categories = categoryRepository.findByDepth(depth);
return categories.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@
import java.util.Optional;

public interface SalesProductRepository extends CrudRepository<SalesProduct, Long> {
@Lock(LockMode.PESSIMISTIC_WRITE)
Optional<SalesProduct> findById(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.mini.joymall.sale.service;

import com.mini.joymall.order.domain.entity.OrderItem;

import java.util.Set;

public interface SalesProductFacade {
void decreaseStock(Set<OrderItem> orderItems);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.mini.joymall.sale.service;

import com.mini.joymall.order.domain.entity.OrderItem;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
@RequiredArgsConstructor
public class SalesProductFacadeImpl implements SalesProductFacade {

private final RedissonClient redissonClient;
private final SalesProductService salesProductService;

private static final String LOCK_KEY_PREFIX = "salesProduct:";

@Override
public void decreaseStock(Set<OrderItem> orderItems) {
for (OrderItem orderItem : orderItems) {
String lockKey = LOCK_KEY_PREFIX + orderItem.getSalesProductId();
RLock lock = redissonClient.getLock(lockKey);

try {
boolean acquireLock = lock.tryLock(10, 1, TimeUnit.SECONDS);

if (!acquireLock) {
throw new RuntimeException("SalesProduct Lock 획득 실패");
}
salesProductService.decreaseStock(orderItem);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Lock 획득 중 인터럽트 발생");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
package com.mini.joymall.sale.service;

import com.mini.joymall.order.domain.entity.OrderItem;
import com.mini.joymall.sale.domain.entity.SalesProduct;
import com.mini.joymall.sale.domain.repository.SalesProductRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.NoSuchElementException;
import java.util.Set;

public interface SalesProductService {
void decreaseStock(Set<OrderItem> orderItems);
@Component
@RequiredArgsConstructor
public class SalesProductService {

private final SalesProductRepository salesProductRepository;

@Transactional
public void decreaseStock(OrderItem orderItem) {
SalesProduct salesProduct = salesProductRepository.findById(orderItem.getSalesProductId())
.orElseThrow(NoSuchElementException::new);
salesProduct.decreaseStock(orderItem.getQuantity());
salesProductRepository.save(salesProduct);
}
}

This file was deleted.

16 changes: 16 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ spring:
username: sa
password:

data:
redis:
host: localhost
port: 6379


sql:
init:
mode: always
Expand Down Expand Up @@ -52,6 +58,11 @@ spring:
username: sa
password:

data:
redis:
host: localhost
port: 6379

sql:
init:
mode: always
Expand Down Expand Up @@ -82,6 +93,11 @@ spring:
username: ${JOY_MALL_DB_USER}
password: ${JOY_MALL_DB_PASSWORD}

data:
redis:
host: ${JOY_MALL_REDIS_URL}
port: 6379

kakao-pay:
host: https://open-api.kakaopay.com
cid: ${KAKAO_PAY_CID}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import com.mini.joymall.sale.domain.repository.SalesGroupRepository;
import com.mini.joymall.sale.domain.repository.SalesProductRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.internal.matchers.Or;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

Expand All @@ -20,59 +20,56 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@SpringBootTest
class SalesProductServiceImplTest {
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class SalesProductFacadeTest {
@Autowired
private SalesProductServiceImpl salesProductServiceImpl;
private SalesProductFacade salesProductFacade;

@Autowired
private SalesGroupRepository salesGroupRepository;

@Autowired
private SalesProductRepository salesProductRepository;

private Long savedSalesProductId;

private Set<OrderItem> orderItems = new HashSet<>();

@BeforeEach
void setUp() {
SalesProduct salesProduct = new SalesProduct(1L, 1000, 100, SalesStatus.ON_SALES);
@Test
public void 판매_상품_동시성_재고_감소_테스트() throws InterruptedException {
// given
SalesProduct salesProduct = new SalesProduct(1L, 1000, 1000, SalesStatus.ON_SALES);

Set<SalesProduct> salesProducts = new HashSet<>();
salesProducts.add(salesProduct);
SalesGroup salesGroup = new SalesGroup(salesProducts);
SalesGroup savedSalesGroup = salesGroupRepository.save(salesGroup);
List<SalesProduct> savedSalesProducts = savedSalesGroup.getSalesProducts().stream().toList();

savedSalesProductId = savedSalesProducts.get(0).getId();
Long savedSalesProductId = savedSalesProducts.get(0).getId();

OrderItem orderItem = new OrderItem(savedSalesProductId, 1, 1000);

Set<OrderItem> orderItems = new HashSet<>();
orderItems.add(orderItem);
}

@Test
void 비관적_락_멀티_쓰레드_테스트() throws InterruptedException {
// given
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(30);
int threadCount = 1000;
ExecutorService executorService = Executors.newFixedThreadPool(700);
CountDownLatch latch = new CountDownLatch(threadCount);

// when
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
salesProductServiceImpl.decreaseStock(orderItems);
} finally {
latch.countDown();
}
try {
salesProductFacade.decreaseStock(orderItems);
} finally {
latch.countDown();
}
});
}
latch.await();

// then
SalesProduct findSalesProduct = salesProductRepository.findById(savedSalesProductId).orElseThrow(NoSuchElementException::new);
Assertions.assertThat(findSalesProduct.getSalesStock()).isEqualTo(0);
assertThat(findSalesProduct.getSalesStock()).isEqualTo(0);
}
}
Loading

0 comments on commit 4abb68b

Please sign in to comment.