본문 바로가기
Dev/[넘블] 2023 백엔드 챌린지

[Numble] Spring으로 타임딜 서버 구축 - 트러블 슈팅과 회고 1 (DB, 아키텍처, 테스트)

by javapp 자바앱 2023. 4. 17.
728x90

 

1. ERD 작성


나름대로 스스로 ERD설계했습니다.

 

  • 타임딜 이라는 것은 상품에서 확장된 느낌이라 생각했습니다.
  • 재설정된 세일가격, 제한물량, 시작시간을 추가했습니다.
  • 타임딜 상품은 일반상품과 동일하게 상품명, 상품설명 등을 가질 수 있다고 생각했습니다.
  • 한 상품으로 여러 타임테이블을 생성할 수 있다고 생각하여 상품과 타임딜 테이블을 1:N으로 설정했습니다.
  • 유저와 상품의 관계테이블로 구매테이블이 생성됩니다.

 

2. API목록


API 명세

각각의 도메인 별로 API 정리

 

 

 

 

3. 와이어프레임


피그마로 작성

 

 

 

4. 아키텍처


  • 젠킨스 서버와 배포 서버를 운영하였습니다.
  • 젠킨스 서버에서 GitHub 코드를 pull 하여 코드를 가져오고 빌드시켜 DockerHub에 Push 합니다.
  • 배포 서버에서 젠킨스를 통해 원격으로 DockerHub의 jar 를 pull & run 하여 도커 컨테이너로 Spring Boot 를 실행합니다.

CI/CD 트러블 슈팅은 8번째 글 참고

 

 

 

5. DB 연동


Naver Cloud 의 "Cloud DB for MySQL 을 사용

 

MySQL Workbench에 연결

 

 

 

6. USER 기능 페이지네이션


JPA를 통해 비교적 간단하게 페이지네이션을 구현할 수가 있었기에 기록해보려고 합니다.

 

6.1. 

UserController

@GetMapping("/v1/users")
public ResponseEntity<Page<UserEntity>> userPagination(@PageableDefault(size = 50)Pageable pageable){
    return new ResponseEntity<>(userService.userPagination(pageable), HttpStatus.OK );
}

localhost:8011/v1/users?page=0

주목해야될 부분은 page 부분인데 파라미터는 (@PageableDefault(size = 50)Pageable pageable) 이렇게 작성돼있는데

요청을 할 때 다음과 같이 요청가능 size 생략 가능

/page=0&size=0  

 

6.2.

UserService

public Page<UserEntity> userPagination(Pageable pageable) {
    PageRequest pr = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize());
    return userRepository.findAll(pr);
}

PageRequest 클래스를 생성하여 findAll의 파라미터로 담는다.

Sort 를 생성해서 한 필드를 지정하여 정렬 가능

{
    "content": [
        {
            "userId": "admin1371",
            "password": "pass",
            "name": "admin",
            "nickname": "admin",
            "phone": "010-1234-1234",
            "email": "admin@naver.com",
            "profile": null,
            "emailCheck": false,
            "grade": null,
            "role": "ROLE_USER",
            "createdDate": "2023-03-13T05:44:44.02354"
        },
        {
            "userId": "client2996",
            "password": "pass",
            "name": "client",
            "nickname": "client",
            "phone": "010-1234-1234",
            "email": "client@naver.com",
            "profile": null,
            "emailCheck": false,
            "grade": null,
            "role": "ROLE_USER",
            "createdDate": "2023-03-13T05:44:44.143916"
        }
    ],
    "pageable": {
        "sort": {
            "sorted": false,
            "unsorted": true,
            "empty": true
        },
        "pageNumber": 0,
        "pageSize": 50,
        "offset": 0,
        "paged": true,
        "unpaged": false
    },
    "last": true,
    "totalPages": 1,
    "totalElements": 2,
    "first": true,
    "number": 0,
    "sort": {
        "sorted": false,
        "unsorted": true,
        "empty": true
    },
    "numberOfElements": 2,
    "size": 50,
    "empty": false
}

 

 

 

7. 테스트 코드 작성


JUnit 5 

 

  • 회원가입
  • 카테고리 등록
  • 상품 등록
  • 타임딜 등록
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class TimedealApplicationTests {
   private final Logger log = LoggerFactory.getLogger(getClass());
   @Autowired
   UserService userService;
   @Autowired
   ProductService productService;
   @Autowired
   CategoryService categoryService;
   @Autowired
   TimedealService timedealService;
   @Autowired
   PurchaseService purchaseService;

   @Test
   @Order(1)
   @DisplayName("회원가입")
   void signup() {
      APIMessage<Boolean> apiMessage = userService.signup(ReqSignup.builder()
            .nickname("admin")
            .name("admin")
            .email("admin@naver.com")
            .phone("010-1234-1234")
            .password("pass")
            .build());
      userService.signup(ReqSignup.builder()
            .nickname("client")
            .name("client")
            .email("client@naver.com")
            .phone("010-1234-1234")
            .password("pass")
            .build());
      // then
      assertTrue(apiMessage.getData());
   }

   @Test
   @Order(2)
   @DisplayName("카테고리 등록")
   void saveCategory(){
      // when
      String categoryCode = "pants-1";
      String categoryName = "바지";
      // given
      CategoryDto categoryDto = categoryService.categoryCreation(new CategoryDto(categoryCode, categoryName));
      //then
      assertEquals(categoryCode,categoryDto.getCategoryCode());
      assertEquals(categoryName,categoryDto.getCategoryName());
   }

   @Test
   @Order(3)
   @DisplayName("상품 등록")
   void registerProduct(){
      // given
      String nickname="admin";
      ReqProduct reqProduct = new ReqProduct();
      String userid= userService.findUserIdByNickname(nickname);
      log.info("userid: {}", userid);
      reqProduct.setUser_id(userid);
      reqProduct.setCategory_code("pants-1");
      reqProduct.setProduct_name("트레이닝 팬츠");
      reqProduct.setProduct_desc("남녀노소 누구나 입을 수 있는 바지 입니다.");
      reqProduct.setProduct_price(30000);
      reqProduct.setSale_price(25000);
      reqProduct.setProduct_amount(10000);

      // when
      RespProduct respProduct = productService.productCreation(reqProduct);

      // then
      assertNotNull(respProduct);
   }

   RespTimedeal respTimedeal = null;

   @Test
   @Order(4)
   @DisplayName("타임딜 등록")
   void registerTimedeal(){
      // given
      List<RespProduct> productList = productService.findProductList();

      if (productList.size() >= 1){
         Long productid = productList.get(0).getProductId();
         ReqTimedeal reqTimedeal = new ReqTimedeal();
         reqTimedeal.setProduct_id(productid);
         reqTimedeal.setLimited_amount(100);
         reqTimedeal.setSale_price(20000);
         reqTimedeal.setStart_datetime(LocalDateTime.of(2023,03,8,00,00));
         respTimedeal = timedealService.createTimedeal(reqTimedeal);
      }
      assertNotNull(respTimedeal);
   }
}

 

 

구매기능에 대한 테스트

@SpringBootTest
public class PurchaseTests {
    private final Logger log = LoggerFactory.getLogger(getClass());
    @Autowired
    UserService userService;
    @Autowired
    ProductService productService;
    @Autowired
    CategoryService categoryService;
    @Autowired
    TimedealService timedealService;
    @Autowired
    PurchaseService purchaseService;

    @Test
    @DisplayName("여러 유저가 구매")
    void manyPplPurchase() throws InterruptedException{
        /**
         * 여러 유저 생성
         * 쓰레드 생성해서
         * 구매 러시
         * */
        // given
        int threadCount = 50;
        String userId = userService.findUserIdByNickname("client");
        ReqPurchase reqPurchase = new ReqPurchase();
        reqPurchase.setUser_id(userId);
        reqPurchase.setTimedeal_id(1L);
        reqPurchase.setCount(2);

        // 멀티스레드 이용 ExecutorService : 비동기를 단순하게 처리할 수 있도록 해주는 java api
        ExecutorService executorService = Executors.newFixedThreadPool(32);

        // 다른 스레드에서 수행이 완료될 때 까지 대기할 수 있도록
        CountDownLatch latch = new CountDownLatch(threadCount);

        // when
        for(int i = 0; i< threadCount; i++){
            executorService.submit(()->{
                try{
                    // 로직
                    purchaseService.purchaseTimedeal(reqPurchase);
                }finally {
                    latch.countDown();
                }
            });
        }
        latch.await();

        // then
        Timedeal timedeal = timedealService.findById(1L);
        assertEquals(0,timedeal.getLimitedAmount());
    }

    @Test
    @DisplayName("비관적 락으로 구매")
    void manyPplPurchaseWithPessimisticLock() throws InterruptedException{
        /**
         * 여러 유저 생성
         * 쓰레드 생성해서
         * 구매 러시
         * */
        // given
        int threadCount = 60;
        String userId = userService.findUserIdByNickname("client");
        ReqPurchase reqPurchase = new ReqPurchase();
        reqPurchase.setUser_id(userId);
        reqPurchase.setTimedeal_id(1L);
        reqPurchase.setCount(2);

        // 멀티스레드 이용 ExecutorService : 비동기를 단순하게 처리할 수 있도록 해주는 java api
        ExecutorService executorService = Executors.newFixedThreadPool(32);

        // 다른 스레드에서 수행이 완료될 때 까지 대기할 수 있도록
        CountDownLatch latch = new CountDownLatch(threadCount);

        // when
        for(int i = 0; i< threadCount; i++){
            executorService.submit(()->{
                try{
                    // 로직
                    purchaseService.purchaseTimedealWithPessimisticLock(reqPurchase);
                }finally {
                    latch.countDown();
                }
            });
        }
        latch.await();

        // then
        Timedeal timedeal = timedealService.findById(1L);
        assertEquals(0,timedeal.getLimitedAmount());
    }

    @Test
    @DisplayName("낙관적 락으로 구매")
    void manyPplPurchaseWithOptimisticLock() throws InterruptedException{
        /**
         * 여러 유저 생성
         * 쓰레드 생성해서
         * 구매 러시
         * */
        // given
        int threadCount = 500;
        String userId = userService.findUserIdByNickname("client");
        ReqPurchase reqPurchase = new ReqPurchase();
        reqPurchase.setUser_id(userId);
        reqPurchase.setTimedeal_id(1L);
        reqPurchase.setCount(2);

        // 멀티스레드 이용 ExecutorService : 비동기를 단순하게 처리할 수 있도록 해주는 java api
        ExecutorService executorService = Executors.newFixedThreadPool(32);

        // 다른 스레드에서 수행이 완료될 때 까지 대기할 수 있도록
        CountDownLatch latch = new CountDownLatch(threadCount);

        // when
        for(int i = 0; i< threadCount; i++){
            executorService.submit(()->{
                try{
                    // 로직
                    purchaseService.purchaseTimedealWithOptimisticLock(reqPurchase);
                }finally {
                    latch.countDown();
                }
            });
        }
        latch.await();

        // then
        Timedeal timedeal = timedealService.findById(1L);
        assertEquals(0,timedeal.getLimitedAmount());
    }
}

 

 

 

 

 

 

 

댓글