세마포어는 다중 스레드 프로그래밍에서 동시에 공유 자원에 접근하는 스레드의 수를 제한하여 동시성 문제를 방지하는 데 사용되는 변수나 추상 데이터 타입입니다.
세마포어는 특정한 수의 토큰을 가지며, 스레드가 세마포어를 획득하려면 토큰을 얻어야 합니다.
토큰이 없다면, 스레드는 대기 상태에 들어갑니다.
세마포어는 주로 두 가지 종류가 있습니다
이진 세마포어와 카운팅 세마포어
이진 세마포어는 0 또는 1의 값을 가지며, 상호 배제를 위해 사용됩니다.
카운팅 세마포어는 0 이상의 값을 가질 수 있으며, 제한된 수의 자원에 대한 접근을 관리합니다.
현제 이 글에서 동시성 문제 해결을 위해서 이진 세마포어를 사용했습니다.

현재 프로젝트를 진행하며 이벤트 신청 페이지를 통해 사용자들이 책을 신청할 수 있는 기능을 제공하고 있습니다.
그러나 성능 테스트를 해보니 다수의 사용자가 동시에 같은 책을 신청할 때 문제가 발생하였습니다.




문제를 파악하기 위해 JMeter를 사용하여 동시성 테스트를 진행하였습니다.
여러 사용자가 동시에 같은 책을 신청하는 시나리오를 설정하여 테스트를 진행한 결과, 하나의 책에 대해 여러 개의 신청이 접수되는 현상을 확인할 수 있었습니다.
이러한 현상은 동시성 오류로 인해 발생하는 것으로 하나의 책은 오직 한 사람에게만 기부될 수 있어야 하지만 여러 사람에게 기부가 되는 것처럼 잘못 처리되었습니다.
이는 서비스의 신뢰성을 해치는 심각한 문제로 즉각적인 해결이 필요했습니다.
우선 세마포어를 사용하여 동시성 문제를 해결하기로 결정하였습니다.
세마포어는 동시에 접근할 수 있는 스레드의 수를 제한하여, 한 번에 하나의 스레드만이 공유 자원에 접근할 수 있도록 하는 동기화 메커니즘입니다.
@Configuration
public class SemaphoreConfig {
@Bean
public Semaphore binarySemaphore() {
return new Semaphore(1, true);
}
}
private final Semaphore semaphore;
@PostMapping("/bookApplyDonation")
public ResponseEntity<MessageDto> createBookApplyDonation(@RequestBody BookApplyDonationRequestDto bookApplyDonationRequestDto){
try{
semaphore.acquire();
return ResponseEntity.ok().body(bookApplyDonationService.createBookApplyDonation(bookApplyDonationRequestDto));
}catch (InterruptedException e) {
e.printStackTrace();
return ResponseEntity.badRequest().body(new MessageDto("나눔 신청에 실패했습니다."));
}finally {
semaphore.release();
}
}
실제 이벤트 신청 로직에 세마포어를 적용하였습니다.

세마포어 적용 후 다시 동시성 테스트를 진행한 결과, 이번에는 모든 사용자가 동일한 책을 신청하였을 때 오직 한 명만이 신청에 성공하고 나머지는 실패하는 것을 확인할 수 있었습니다.
이를 통해 동시성 문제가 성공적으로 해결되었다는 것을 알 수 있었습니다!!!




결과를 보면 95% ~ 99 %
0.6 ~ 0.85초 정도 걸리는 것을 확인 할 수 있다.
예상보다 너무 느리게 나오는 걸 확인할 수 있다.
하지만 이건 회원가입과 로그인을 진행한 이후라 좀 더 느리게 나온 것일 수도 있다.
현재는 조금 더 시각적으로 보기 쉽게 예외를 발생 시켰기에 성능이 조금 작게 나오는 걸로 추정됩니다.
예외를 DTO를 리턴 하는 것으로 바꿔보겠습니다.


테스트 결과 0.5~ 0.7초 사이가 나오는 것을 확인 할 수 있습니다.

0.3 ~ 0.7 초 사이가 나오는 것을 알 수 있습니다.
즉 성능 테스트 결과 세마포어를 사용하면 동시성 문제를 해결할 순 있지만 약간의 성능이 저하 된다는 것을 알 수 있습니다.