안녕하세요!.. 강의와 무관한 내용이지만 마땅히 스프링 관련된 질문을 아는 곳이 없어 평소 스프링 강의를 들은 선생님께 질문을 하게 되었습니다.
질문에 대한 전체 코드는 아래 링크에 작성했습니다.
https://www.notion.so/7iwook/userVolunteerService-1d911afe2d6545bfa1e8247cc6006748?pvs=4
@Lock(LockModeType.PESSIMISTIC_READ)
@Override
public Optional<UserVolunteerWork> findByVolunteerWorkIdAndUserId(Long volunteerWorkId, Long userId) {
return Optional.ofNullable(queryFactory
.selectFrom(userVolunteerWork)
.join(userVolunteerWork.volunteerWork, volunteerWork)
.join(userVolunteerWork.user, user).fetchJoin()
.where(
userVolunteerWork.volunteerWork.id.eq(volunteerWorkId)
.and(userVolunteerWork.user.id.eq(userId))
).setLockMode(LockModeType.PESSIMISTIC_READ)
.fetchOne());
}
위 코드를 순차적 시행하면 문제없이 원하는 결과값을 반환하지만 (테스트 코드가 길어 똑같은 부분을 제외하고 차이점만 올립니다.)
for (int i = 0; i < tryCnt; i++) {
User user = users.get(i);
try {
volunteerService.approve(adminEmail, volunteerWorkId, user.getId());
successCount++; // 성공한 신청 수 증가
} catch (Exception e) {
failedCount++;
}
}
동시성 테스트를 위해 아래와 같이 구성을 하면 쿼리 결과 값이 null로 나와 EntityNotFoundException을 던지게 됩니다..
int numThreads = 50; // 50명의 유저가 동시에 신청
CountDownLatch doneSignal = new CountDownLatch(numThreads);
ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
AtomicInteger successCount = new AtomicInteger();
AtomicInteger failCount = new AtomicInteger();
for (int i = 0; i < numThreads; i++) {
User user = users.get(i % users.size()); // 유저 목록에서 순차적으로 유저를 가져옴
executorService.execute(() -> {
try {
log.info("approved userId = {}", user.getId());
volunteerService.approve(adminEmail, volunteerWorkId, user.getId());
successCount.getAndIncrement(); // 성공한 신청 수 증가
} catch (Exception e) {
e.printStackTrace();
failCount.getAndIncrement();
// 예외 처리
} finally {
doneSignal.countDown(); // 쓰레드 작업 완료를 알림
}
});
}
인터넷에 나와있는 비관적, 낙관적 락을 모두 시도해보았으나 null로만 반환되는 현상이 나오는 데 이 현상이 동시성에 있는건지도 아직 모르겠습니다... 원인에 대해 찾아주신다면 정말 감사합니다!!
좋은 강의 항상 잘 듣고 있습니다!!
안녕하세요. tizmfns1218님
스프링 핵심원리 고급편에서 설명드린 ThreadLocal 내용과 관련이 있는데요.
스프링의 트랜잭션은 스레드 단위로 생성되고 사용된다고 이해하시면 됩니다.
만약 별도의 스레드를 생성했다면, 같은 트랜잭션이 이어지지 않습니다.
따라서 만약 테스트 코드에서 트랜잭션을 시작했다면 테스트 코드에서 시작한 트랜잭션과 volunteerService.approve() 메서드에서 시작한 트랜잭션이 단일 스레드 상황에서는 하나로 연결되지만, volunteerService.approve()를 다른 쓰레드에서 실행하게 되면 둘은 완전히 별도의 트랜잭션에서 실행되어 버립니다.
테스트 코드에서 실행한 트랜잭션이 아직 데이터를 커밋하지 않았기 때문에 별도의 스레드에서 해당 데이터를 찾아오지 못하는 것이지요.
이 문제를 해결하려면 단일 스레드를 사용하거나 또는, 테스트를 위해 저장하는 기본 데이터를 확실히 커밋하도록 만든 다음에 다른 스레드에서 해당 데이터를 조회해야 합니다. 그리고 테스트 마지막에는 기존 데이터를 잘 정리해서 지우면 되겠지요?
감사합니다.
답글