정해진 시간에 제한된 개수의 물건을 파는 쇼핑몰 기능인 타임딜 서버 구축
2️⃣ 와이어프레임
피그마를 사용하여 추가적인 화면을 디자인하고 추가하였습니다.
Jenkins와 Naver Cloud Server를 사용해 CI/CD를 구현했습니다. 구조는 다음과 같습니다.
- 젠킨스 서버와 배포 서버를 하나씩 운영(인스턴스 2개)
- 젠킨스 서버에서 GitHub 코드를 pull 하여 코드를 가져오고 빌드시켜 DockerHub에 Push
- 배포 서버에서 젠킨스를 통해 원격으로 DockerHub의 jar 를 pull & run 하여 도커 컨테이너로 Spring Boot 를 실행
핵심기능을 구현할 수 있도록 설계
- users(유저), products(상품), category, timedeal(타임딜), purchase(구매) 총 5개 테이블로 구성
- 타임딜 상품은 일반상품과 별개 테이블로 관리하도록 함
동시성 문제 : 변경되는 데이터에 의해 발생한다고 생각(변경되기 전의 데이터에 대한 접근과 변경된 후에 데이터에 대한 접근에 대한 데이터 정합성 문제)
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을 사용하여 여러 서버가 있다고 가정하여 처리를 하는 것도 좋다고 생각하지만 외부 시스템을 사용하지 않음
- 1만개의 재고가 있다고 가정
- Pessimistic Lock을 걸어 동시성 처리
TPS : 127.4
Error 도 발생
핀포인트 확인
실패한 부분이 있었고 추적결과 약 30초간 기다렸다가 실패한 것으로 보인다.
hikari:
connection-timeout: 300000
server:
tomcat:
threads:
max: 200
connection-timeout을 대폭 늘렸다.
- pool에서 커넥션을 얻어오기전까지 기다리는 최대 시간, 허용가능한 wait time을 초과하면 SQLException을 던짐
TPS: 160.5으로 올랐다.
쓰레드 max를 300으로 올렸는데, TPS가 120으로 나왔다.
쓰레드 간 경합으로 쓰레드 최대값을 늘리면 쓰레드 간 경합이 발생할 가능성이 높아지고 여러 쓰레드가 같은 자원에 접근하면 경합이 발생하여 cpu 느리게 처리되는 것 같다.
구매 기능에 대해 멀티쓰레드를 이용하여 비동기로 테스트
@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());
}