프로젝트를 진행하다 보면 트래픽이 몰릴 경우 동시성 문제가 발생할 수 있다.
그러기 위해선 다양한 방법을 사용할 수 있는데 첫 번째로 나는 DB LOCK을 걸어볼 것이다.
상황은 다음과 같다.
1. 티켓 서비스를 제공하고 티켓을 n장을 준비해 둔다.
2. 스레드로 동시 접속으로 티켓을 예매할 것이다.
LOCK에도 다양한 종류가 있지만 나는 비관적락(PESSIMISTIC_WRITE)을 사용해서 해결해 볼 것이다.
그 이유는 동시성 충돌이 예상되기 때문에 해당 데이터에 접근하지 못하도록 하기 위함이다.
코드
(JPA 사용)
TicketController
@RestController
@RequiredArgsConstructor
@RequestMapping("/ticket")
@Slf4j
public class TicketController {
private final TicketService ticketService;
// step 1) 데이터 베이스 락
@PostMapping("/lock")
public ResponseEntity<String> reserveTicket(@RequestBody TicketReserveRequest ticketReserveRequest) throws TicketSoldOutException {
log.info("POST " + ticketReserveRequest.getTicketId() + ", " + LocalDateTime.now());
ticketService.reverseTicketLock(ticketReserveRequest.getTicketId());
return ResponseEntity.ok("티켓 예약 성공");
}
// 티켓 생성
@PostMapping
private ResponseEntity<String> autoCreateTicket(@RequestBody TicketRequest ticketRequest) {
ticketService.save(ticketRequest);
return ResponseEntity.ok("티켓 생성 성공");
}
}
TicketService
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class TicketService {
private final TicketRepository ticketRepository;
public Long save(TicketRequest ticketRequest) {
return ticketRepository.save(Ticket.builder()
.ticketName(ticketRequest.getTicketName())
.ticketMaxCount(ticketRequest.getTicketMaxCount())
.build()).getId();
}
public TicketResponse findById(Long id) {
return TicketResponse.from(ticketRepository.findById(id).orElseThrow(TicketNotFoundException::new));
}
// step 1) Pessimistic Lock: 데이터를 읽거나 수정하기 전에 해당 데이터를 락으로 설정하는 방식
@PersistenceContext
private EntityManager entityManager;
public void reverseTicketLock(Long ticketId) {
Ticket ticket = entityManager.find(Ticket.class, ticketId, LockModeType.PESSIMISTIC_WRITE);
ticket.currentCountIncrement();
log.info("잔여 티켓: " + (ticket.getTicketMaxCount() - ticket.getTicketCurrentCount()));
}
}
위와 같이 EntityManager를 통해 로직이 끝날 때까지 락을 걸어줬다.
이렇게 하면 끝이다.
테스트 코드
TicketServiceTest
@SpringBootTest
class TicketServiceTest {
@Autowired
private TicketService ticketService;
private Long id1 = null;
private Integer maxCount = 100;
@BeforeEach
public void before() {
TicketRequest ticket1 = new TicketRequest("ticket1", maxCount);
id1 = ticketService.save(ticket1);
TicketRequest ticket2 = new TicketRequest("ticket2", maxCount);
id2 = ticketService.save(ticket2);
}
@Test
void 동시성_문제_LOCK() throws InterruptedException {
int peopleCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch tt = new CountDownLatch(peopleCount);
for (int i = 0; i < peopleCount; i++) {
executorService.execute(() -> {
ticketService.reverseTicketLock(id1);
tt.countDown();
});
}
tt.await();
TicketResponse ticket = ticketService.findById(id1);
Assertions.assertThat(ticket.getTicketCurrentCount()).isEqualTo(maxCount);
}
}
실행 결과
잘 해결되는 것을 볼 수 있다.
하지만 주의해야 하는 점은 DB와 서버에 부담이 많이 된다.
그래서 다음 Step으로 분산락(Redisson)을 사용해서 부담을 줄일 것이다.
다음 step)
'JAVA > DB' 카테고리의 다른 글
[DB] 스프링 트랜잭션(@Transactional)이란? 사용해보기 (feat. JdbcTemplate) (0) | 2022.09.28 |
---|---|
[DB] 커넥션 풀(Connection Pool)이란? HikariCP 사용해보기 (0) | 2022.09.26 |