2025. 4. 3. 23:24ㆍBackend/Spring
개요
프로젝트에서 동시성 제어를 mvp를 만들면서 생긴 레이스 컨디션에 대한 이야기를 정리를 해보려고 한다.
소스 코드
@Transactional
public void payment(/*Long userId, */Long bookId) {
Book book;
RLock lock = redissonClient.getFairLock("book:"+bookId);
try {
boolean acquired = lock.tryLock(10L, 1L, TimeUnit.SECONDS);
if (!acquired) {
throw new InterruptedException();
}
book = bookRepository.findById(bookId).orElseThrow(
() -> new NotFoundException(ErrorMessage.NOT_FOUND_BOOK.getMessage())
);
// log.info("" +book.getCount());
book.CountMinusOne();
log.info("" +book.getCount());
//오더 로직 들어갈 예정
} catch (InterruptedException ex) {
throw new InvalidRequestException(ErrorMessage.REDIS_ERROR.getMessage());
} catch (Exception ex) {
throw new InvalidRequestException(ErrorMessage.ERROR.getMessage());
} finally {
if( lock.isLocked() )
lock.unlock();
}
}
간단하게 만들어두고 동시성 문제를 해결을 하려고 진행을 하였다.
위의 내용은 공식문서을 참고를 해서 진행을 하였다.
레디스 공식 문서
redisson-examples/locks-synchronizers-examples/src/main/java/org/redisson/example/locks/FairLockExamples.java at master · redis
Redisson java examples. Contribute to redisson/redisson-examples development by creating an account on GitHub.
github.com
다음 내용중에 페어 락에 대한 예시가 존재한다.
package org.redisson.example.locks;
import java.util.ArrayList;
import java.util.List;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
public class FairLockExamples {
public static void main(String[] args) throws InterruptedException {
// connects to 127.0.0.1:6379 by default
RedissonClient redisson = Redisson.create();
RLock lock = redisson.getFairLock("test");
int size = 10;
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < size; i++) {
final int j = i;
Thread t = new Thread() {
public void run() {
lock.lock();
lock.unlock();
};
};
threads.add(t);
}
for (Thread thread : threads) {
thread.start();
thread.join(5);
}
for (Thread thread : threads) {
thread.join();
}
}
}
다음 내용을 참고를 하길 바란다.
테스트 코드
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
@Mock
private BookRepository bookRepository;
@InjectMocks
private PaymentService paymentService;
@Mock
private RedissonClient redissonClient;
@Mock
private RLock rlock;
@Test
@DisplayName("단일 호출 테스트")
void paymentTest() throws InterruptedException {
//given
Book book = new Book();
ReflectionTestUtils.setField(book, "bookId", 1L);
ReflectionTestUtils.setField(book, "count", 1000L);
given(bookRepository.findById(anyLong())).willReturn(Optional.of(book));
given(redissonClient.getFairLock("book:"+book.getBookId())).willReturn(rlock);
given(rlock.tryLock(10L, 1L, TimeUnit.SECONDS)).willReturn(true);
// when
paymentService.payment(1L);
// then
verify(bookRepository).findById(1L);
assertEquals(999, book.getCount());
}
@Test
@DisplayName("멀티 쓰래드 테스트")
void paymentTest_thread() throws InterruptedException {
//given
Book book = new Book();
ReflectionTestUtils.setField(book, "bookId", 1L);
ReflectionTestUtils.setField(book, "count", 1000L);
int threadCount = 1000;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
given(redissonClient.getFairLock("book:"+book.getBookId())).willReturn(rlock);
given(bookRepository.findById(anyLong())).willReturn(Optional.of(book));
given(rlock.tryLock(10L, 1L, TimeUnit.SECONDS)).willReturn(true);
//when
for (int i = 0; i < threadCount; i++) {
executorService.execute(() -> {
try {
paymentService.payment(1L);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
assertEquals(0, book.getCount());
}
}
이렇게 작성을 진행을 하였다.
단일 테스트는 동시성 제어에 대한 문제가 안생기니 넘기도록 하고, 멀티 쓰레드 테스트에 대해서 살펴 보도록하겠다.
멀티 쓰레드 테스트
@DisplayName("멀티 쓰래드 테스트")
void paymentTest_thread() throws InterruptedException {
//given
Book book = new Book();
ReflectionTestUtils.setField(book, "bookId", 1L);
ReflectionTestUtils.setField(book, "count", 1000L);
int threadCount = 1000;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
given(redissonClient.getFairLock("book:"+book.getBookId())).willReturn(rlock);
given(bookRepository.findById(anyLong())).willReturn(Optional.of(book));
given(rlock.tryLock(10L, 1L, TimeUnit.SECONDS)).willReturn(true);
//when
for (int i = 0; i < threadCount; i++) {
executorService.execute(() -> {
try {
paymentService.payment(1L);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
assertEquals(0, book.getCount());
}
테스트 결과
문제가 없이 통과를 한다...?
테스트를 돌린 환경은 리눅스 환경인데, 원래 메인 환경인 리눅스에서는 동시성 이슈를 통과를 하지 못하였다.
1000개를 하나씩 빼는 로직을 동시성 제어를 해서 여러개의 멀티쓰레드가 총 1000번을 돌리는데
리눅스 환경에서는 3~16개 정도가 항상 남았다.
근대 맥 환경에서는 돌아가는 것이 왜 그런 것인지 모르겠다.
같은 리눅스 베이스여서 비슷할 탠데 이유를 잘 모르겠다.
결과
다음에 리눅스와 맥에서 왜 결과 값이 다른지를 자세하게 찾아봐야겠다.
이유는 서버에서는 보통 맥 서버를 돌리는 것이 아닌 리눅스 서버 ubuntu나 arch를 사용할 것이기 때문이다.
'Backend > Spring' 카테고리의 다른 글
[Spring] @Valid 와 @Validated 의 사용 방법 (1) | 2025.09.02 |
---|---|
[Spring] @JsonCreator 와 @JsonProperty 을 사용해야하나? (2) | 2025.08.28 |
[Spring] 트러블 슈팅 @RequestParam name없으면 생기는 문제 (0) | 2025.03.21 |
[Spring] OutSourcing 프로젝트를 마치며.. (1) | 2025.03.07 |
[Spring] 테스트 코드를 작성을 해보자. (0) | 2025.02.27 |