@Test
void test1() throws InterruptedException {
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
List<Member> memberList = new ArrayList<>();
for(int i =0; i<100; i++){
String email = "user"+i+"@email.com";
String nickName = "user"+i;
memberList.add(new Member(email, nickName, Role.ROLE_USER, null));
}
memberRepository.saveAll(memberList);
for (int i = 0; i < threadCount; i++) {
int index = i;
executorService.submit(() -> {
try{
Member member = memberList.get(index);
bookmarkService.createBookMark(member.getEmail(), bin1.getId());
} finally {
System.out.println("북마크"+index);
countDownLatch.countDown();
}
}
);
}
countDownLatch.await();
Bin bin = binRepository.findById(bin1.getId()).get();
assertThat(bin.getBookmarkCount()).isEqualTo(100L);
}
테스트 시나리오는 쓰레기통 하나에 100명의 사람이 동시에 좋아요를 누르는 것이다.
그런데, 북마크의 카운트가 아예 집계되지 않았다.

테스트내에서 쓰레기통을 저장하는 쿼리들은 DB로 계속 날라갔다.

그런데, 테스트에서 디버깅을 찍고 BookmarkService의 createBookmark를 들어가보면 쓰레기통과 멤버 둘다 찾을 수 없다고 나왔다.


조회 쿼리를 타고타고 들어가면
JPA에서 조회하는 엔티티가 하나도 없는 것으로 보였다.
DB에 저장 자체가 안 된건가? 하는 의문이 들어서 테스트코드와 북마크 서비스에서 BinRepository의 findAll()을 사용해서 모든 쓰레기통을 다 조회해봤다.

테스트코드 내에서는 BinRepository에 엔티티들이 있었다.

북마크 서비스 내에서는 BinRepository에 엔티티들이 없었다..!
트랜잭션과 관련된 것일수도 있겠다는 생각이 들었다.
트랜잭션과 관련된 내용으 찾아봤다.
The PersistenceContext is a short-lived, transaction-scoped context for managing the lifecycle of entities. It represents a set of managed entities stored in memory as a first-level cache of the entity manager. If a transaction begins, the persistence context is created and eventually is closed or cleared when the transaction commits or rolls back.
출처 : PersistenceUnit vs. PersistenceContext
영속성 컨텍스트가 트랜잭션이 시작하면 생성되고, 사라진다는 것이다.
내용을 추가로 확인해보니 테스트를 시작하는 스레드와 ExecutorService의 트랜잭션이 다르다고 한다. 이에 따라 엔티티매니저와 엔티티매니저가 관리하는 영속성 컨텍스트가 달라지는 것으로 보였다.
트랜잭션마다 각각 1차 캐시가 다른탓에 테스트를 시작한 스레드와 북마크 서비스에서 createBookmark를 하는 스레드들 간에 데이터가 달랐던 것이다.

북마크 서비스에서 호출한 BinRepository의 findAll()을 타고타고 들어가면 영속성 컨텍스트가 비어져 있는 걸 알 수 있었다.

사진을 보면 나와있듯이 영속성 컨텍스트에 엔티티들이 잘 담겨져 있다.
새로운 트랜잭션이 시작되기 전에 커밋을 하도록 하면 된다고 한다. 그래야, 영속성 컨텍스트에 있는 1차 캐시 내용이 DB에 반영이 된다. MySQL의 기본 격리 수준인 REPEATABLE READ은 실제 DB에 커밋된 것들을 기준으로 읽는다.
프로덕션 코드를 Read Uncommitted으로 변경하는 방법이 있지만 이건 너무 위험하다.
커밋되지 않은 데이터를 다른 트랜잭션에서 읽으면 당연히 데이터 정합성이 무너진다.
또 다른 방법으로는 테스트의 트랜잭션을 관리하는 TestTransaction 객체로 즉시 커밋하는 것이 있다.
TestTransaction.flagForCommit();
TestTransaction.end();
테스트 코드에서 이렇게 커밋을 해주는 방식이다.

이제 다른 트랜잭션에서 무사히 조회되는 것을 볼 수 있다.
트랜잭션에서 커밋을 해버리니 그렇게 DB에 들어간 레코드들이 롤백이 자동으로 되지 않았다.
EntityManager로 flush()를 해줬을 때는 자동으로 롤백이 됐는데
커밋한 건 롤백이 안 돼서... 어떤 차이가 있는건지 헷갈렸다.
이 내용은 여기에 정리해둔다.