Sith-call / numble-timedeal-server

넘블챌린지-Spring으로 타임딜 서버 구축하기

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

타임딜 서버 구축, 넘블챌린지

정해진 시간에 제한된 개수의 물건을 파는 쇼핑몰 기능인 타임딜 서버 구축


1️⃣ 기술 스택

   


   


피그마를 사용하여 추가적인 화면을 디자인하고 추가하였습니다.


3️⃣ 아키텍처 | 지속적 통합/지속적 배포(CI/CD)

Jenkins와 Naver Cloud Server를 사용해 CI/CD를 구현했습니다. 구조는 다음과 같습니다.

  • 젠킨스 서버와 배포 서버를 하나씩 운영(인스턴스 2개)
  • 젠킨스 서버에서 GitHub 코드를 pull 하여 코드를 가져오고 빌드시켜 DockerHub에 Push
  • 배포 서버에서 젠킨스를 통해 원격으로 DockerHub의 jar 를 pull & run 하여 도커 컨테이너로 Spring Boot 를 실행

4️⃣ API 명세

📑API 명세


5️⃣ ER-Diagram

핵심기능을 구현할 수 있도록 설계

  • users(유저), products(상품), category, timedeal(타임딜), purchase(구매) 총 5개 테이블로 구성
  • 타임딜 상품은 일반상품과 별개 테이블로 관리하도록 함

6️⃣ 동시성처리

동시성 문제 : 변경되는 데이터에 의해 발생한다고 생각(변경되기 전의 데이터에 대한 접근과 변경된 후에 데이터에 대한 접근에 대한 데이터 정합성 문제)

Race condition을 해결하는 여러 방법이 있다. synchronized, MySql Lock, 메시지 브로커(Redis pub/sub, kafka)


JPA의 낙관적 락과 비관적 락을 통해 엔티티에 대한 동시성 제어

Pessimistic Lock && Optimistic Lock

여러 서버에서 동시에 같은 레코드를 업데이트하려고 할 때 발생하는 동시성 문제를 해결하는 방법
Pessimistic Lock: 데이터베이스의 락을 사용하여 동시성 제어

@Lock(value = LockModeType.PESSIMISTIC_WRITE)
@Query("select t from Timedeal t where t.timedealId = :timedealId")
Timedeal findByIdWithPessimisticLock(@Param("timedealId") Long timedealId);

  • synchronized는 서버가 여러 대 일때 동시성 처리가 불가하기 때문에 제외
  • 현재 상황에서 가장 적합한 해결책을 제시하고 실행하는 것이 좋다고 생각했기 때문에
    Redis pub/sub을 사용하여 여러 서버가 있다고 가정하여 처리를 하는 것도 좋다고 생각하지만 외부 시스템을 사용하지 않음

7️⃣ 성능 측정 및 개선내용

성능 측정

서버 상태를 모니터링할 수 있는 APM 툴인 pinpoint 도입

nGrinder 테스트

  • 1만개의 재고가 있다고 가정
  • Pessimistic Lock을 걸어 동시성 처리

threads-max = 100 으로 설정
TPS : 127.4

Error 도 발생

개선내용

핀포인트 확인 image 실패한 부분이 있었고 추적결과 약 30초간 기다렸다가 실패한 것으로 보인다.

커넥션 에러 줄이기

    hikari:
      connection-timeout: 300000

server:
  tomcat:
    threads:
      max: 200

connection-timeout을 대폭 늘렸다.

  • pool에서 커넥션을 얻어오기전까지 기다리는 최대 시간, 허용가능한 wait time을 초과하면 SQLException을 던짐

구매기능 테스트

TPS: 160.5으로 올랐다.


쓰레드 max를 300으로 올렸는데, TPS가 120으로 나왔다.
쓰레드 간 경합으로 쓰레드 최대값을 늘리면 쓰레드 간 경합이 발생할 가능성이 높아지고 여러 쓰레드가 같은 자원에 접근하면 경합이 발생하여 cpu 느리게 처리되는 것 같다.


8️⃣ Test(JUnit5)

구매 기능에 대해 멀티쓰레드를 이용하여 비동기로 테스트

@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());
    }

9️⃣ 트러블 슈팅과 회고

[Numble] Spring으로 타임딜 서버 구축하기 - 트러블 슈팅과 회고

About

넘블챌린지-Spring으로 타임딜 서버 구축하기


Languages

Language:Java 99.2%Language:Dockerfile 0.8%