멀티쓰레드 환경에서 querydsl 사용하기에 사용할 예제코드를 작성하다가 생긴 문제에 대한 글입니다.
이로 인해 아주 약간은 이번 주제에 불필요한 코드가 들어있지만 천천히 읽으면 이해하기 어렵지 않다고 생각해 그대로 사용했습니다.
Entity와 Repository에 대한 코드는 설명에서 생략했습니다.
@Service
@RequiredArgsConstructor
public class Service {
private final Repository repo;
@Transactional
public Dto getAData(){
return repo.findAFetchFirst();
}
@SpringBootTest
@Slf4j
@Transactional
public class deadlock {
@Autowired
Service service;
@Autowired
EntityManager em;
@BeforeEach
void initData(){
B b = B.builder().id(1L).var1(99).build();
A a = A.builder().id(1L).var1(1).build();
b.setA(a);
a.setB(b);
em.persist(a);
em.persist(b);
em.flush();
em.clear();
log.info("Data insert End");
}
@Test
void getDataWithoutOtherThread(){
Dto aData = service.getAData();
System.out.println("aData = " + aData);
Assertions.assertNotNull(aData);
}
@Test
void getDataInMyThread() throws InterruptedException {
Runnable userA = () -> {
log.info("thread A start");
Dto aData = service.getAData();
log.info("aData = {}",aData);
Assertions.assertNull(aData);
};
Thread threadA = new Thread(userA);
threadA.start();
threadA.join();
}
비교적 직관적인 코드라고 생각합니다. 내용을 살펴보면
initData()
에서 데이터를 삽입합니다. initData()
는 @BeforeEach
기 때문에 다른 테스트가 실행되기 전에 같이 호출됩니다.getDataWithoutOtherThread()
는 service를 이용해 데이터를 호출합니다. 결과가 잘 출력되는 것도 확인할 수 있습니다.getDataInMyThread()
는 getDataWithoutOtherThread
에서와 동일한 호출을 threadA에서 실행합니다. 이 경우 sevice.getAData()는 null을 반환합니다.
이 문제는 트랜잭션과 관련있습니다. 우선 멀티 쓰레드보다 익숙한 일반적인 상황에서 추가적인 테스트를 진행해 보겠습니다.
@Service
@RequiredArgsConstructor
public class Service {
private final Repository repo;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Dto getDataForTransactionCheckRequiresNew(){
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("getDataForTransactionCheckRequiresNew's actualTransactionActive = " + currentTransactionName);
return repo.findAFetchFirst();
}
@Transactional
public Dto getDataForTransactionCheckBasicPropagation(){
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("getDataForTransactionCheckBasicPropagation's actualTransactionActive = " + currentTransactionName);
return repo.findAFetchFirst();
}
@Test
void propagationTest(){
Dto dataForTransactionCheckBasicPropagation = service.getDataForTransactionCheckBasicPropagation();
Dto dataForTransactionCheckRequiresNew = service.getDataForTransactionCheckRequiresNew();
Assertions.assertNotNull(dataForTransactionCheckBasicPropagation);
Assertions.assertNull(dataForTransactionCheckRequiresNew);
}
크게 달라진 건 없습니다. service는 이전과 동일한 역할을 수행합니다. 다만 중간에 현재 트랜잭션의 이름을 출력하는 코드가 추가되었습니다.
getDataForTransactionCheckRequiresNew
는 트랜잭션의 propagation옵션을 Propagation.REQUIRES_NEW
로 설정했습니다. 이 경우 항상 새로운 트랜잭션을 생성
합니다. 반면 dataForTransactionCheckBasicPropagation
은 propagation옵션을 설정하지 않은 상태로 REQUIRED
가 적용돼 부모 트랜잭션이 존재한다면 부모 트랜잭션에 합류, 그렇지 않다면 새로운 트랜잭션을 만듭니다.
BeforeEach의 트랜잭션은 실행중인 테스트코드의 트랜잭션과 동일
합니다.getDataForTransactionCheckRequiresNew
는 Service.getDataForTransactionCheckRequiresNew
라는 트랜잭션을 생성했고, dataForTransactionCheckBasicPropagation
는 부모 트랜잭션인 multiThread.deadlock.propagationTest
에 합류
한걸 알 수 있습니다.dataForTransactionCheckBasicPropagation
는 null을 반환하지 않았고, dataForTransactionCheckRequiresNew
는 null을 반환했습니다.이를 통해 우리는 BeforeEach가 실행되는 기존의 트랜잭션과 다른 트랜잭션에서는 데이터를 성공적으로 불러올 수 없다
라는 걸 알게 되었습니다.
여기까지 확인하고 멀티 쓰레드 테스트코드를 작성해 봅시다.
@Test
void multiThreadTransactionCheck() throws InterruptedException {
Dto data1 = service.getDataForTransactionCheckBasicPropagation();
System.out.println("outTransaction = " + data1);
Runnable userA = () -> {
Dto data2 = service.getDataForTransactionCheckBasicPropagation();
System.out.println("inTransaction = " + data2);
};
Thread threadA = new Thread(userA);
threadA.start();
threadA.join();
}
MANDATORY
라는 옵션이 있습니다.READ_UNCOMMITTED
로 설정합니다. 문제의 원인은 결국 데이터를 삽입한 트랜잭션이 커밋되지 않은 상태에서 새로운 쓰레드의 새로운 트랜잭션이 해당 데이터를 보려고 시도해서 발생한 문제입니다. 커밋되지 않은 데이터를 보기 위해 isolation level을 낮출 수 있습니다.멀티쓰레드 환경에서 querydsl의 deadlock
이라는 다른 문제에 관한 코드지만 참고해볼만 합니다.새롭게 생성한 쓰레드에서 조회
하려고 하면 조회되지 않습니다.BeforeEach부터 테스트코드가 끝날 때까지 데이터가 실제 DB에 커밋되지 않으며
트랜잭션이 다른 상태에서 커밋되지 않은 데이터를 조회하는게 불가능
하기 때문입니다.