
현재 100명의 TestUser를 생성한 이후 하나의 Todo에 좋아요를 누르려고 한다.
이에 executorService 를 사용하여 테스트를 진행하려고 했지만 수없이 많은 오류를 겪게 되어 기록한다.
@SpringBootTest
@Transactional
class RedisTodoLikeServiceTest {
@Autowired
private RedisTodoLikeService redisTodoLikeService;
@Autowired
private TodoRepository todoRepository;
@Autowired
private UserRepository userRepository;
private Todo testTodo;
private List<User> testUsers = new ArrayList<>();
@BeforeEach
public void before() {
System.out.println("RedisTodoLikeService Test 사전 작업을 진행합니다.");
// 테스트용 유저 생성 및 데이터베이스에 저장
for (int i = 1; i <= 100; i++) {
User user = new User("user" + i + "@example.com", "user" + i, "password");
userRepository.save(user);
testUsers.add(user);
System.out.println("Created user: " + user.getId());
}
// 테스트용 투두 생성 및 데이터베이스에 저장
testTodo = new Todo();
testTodo.setContents("테스트 목업 투두");
todoRepository.save(testTodo);
}
기존에 하듯이 다음과 같이 테스트를 시작하기전 테스트 유저 100명을 DB에 기록 후 하나의 TODO를 생성했다.

다음과 같이 출력을 해보면 잘 심어진 것을 확인할 수 있다.
@Test
@DisplayName("100명의 유저가 투두에 좋아요를 클릭합니다.")
public void testConcurrentLikes() throws Exception {
// given
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(100);
CountDownLatch latch = new CountDownLatch(threadCount);
// when
for (User user : testUsers) {
executorService.execute(() -> {
try {
if (currentUser != null) {
User currentUser = userRepository.findById(user.getId()).orElse(null);
redisTodoLikeService.likeTodo(testTodo.getId(), currentUser.getId());
} else {
System.out.println("User not found: " + user.getId());
}
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
// then
Todo likedTodo = todoRepository.findById(testTodo.getId()).orElse(null);
assertNotNull(likedTodo);
assertEquals(100, likedTodo.getLikes());
}
100개의 스레드를 생성하여 스레드 환경에서 좋아요 카운트를 실행한다.
이때 문제점이 발생하게 된다.

현재 유저에 대한 값이 전부 null로 계속해서 출력이 되었다.
currentUser를 찾아올 수 없어서 콘솔 디버깅을 시작했다.


위에서 출력해본 결과 @BeforeEach에서 삽입한 데이터 형태가 틀리지 않았다는 것을 확신했다.
@Test
@DisplayName("100명의 유저가 투두에 좋아요를 클릭합니다.")
public void testConcurrentLikes() throws Exception {
// given
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(100);
CountDownLatch latch = new CountDownLatch(threadCount);
// when
for (User user : testUsers) {
// 위치 변경
User currentUser = userRepository.findById(user.getId()).orElse(null);
executorService.execute(() -> {
try {
if (currentUser != null) {
redisTodoLikeService.likeTodo(testTodo.getId(), currentUser.getId());
} else {
System.out.println("User not found: " + user.getId());
}
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
// then
Todo likedTodo = todoRepository.findById(testTodo.getId()).orElse(null);
assertNotNull(likedTodo);
assertEquals(100, likedTodo.getLikes());
}
계속해서 시도를 했는데 여전히 User not found만 무한정으로 지속되다가 executorService 밖으로 현재 유저를 찾는 코드를 빼보았다.


executorService 위에서 올바르게 찾아오는 모습을 확인할 수 있었다.
그러면 이제 해결이 될줄 알았다..

이번에는 서비스 로직에서 오류가 다시 한번 발생한 것이다.

위에서 한번 겪었더니 바로 확신할 수 있었다.
이번에는 executorService 안에서 동작한 이 likeTodo에서 현재 유저를 찾는 findById가 또 돌아가지 않아서 null로 발생하는 것이라고 확신했다.
정말 허탈했지만 생각을 해보면서 생각보다 너무 복잡했다.

@Transactional을 제거해주었더니 모든 DB단의 로직이 정상적으로 돌아가고 모든 오류가 사라졌다.
아직 이것에 대해 확신은 하지 못해서 내가 이해한 생각을 적으려고한다.
@Transactional이 붙지 않을 경우 쿼리는 그 즉시 DB에 커밋이 된다.
@Transactional을 붙였을 경우 메소드가 종료될 때 커밋이 되며 진행과정 속에서 오류가 발생할 시 롤백을 한다.
ExecutorSerivce 속에서 나는 100개의 스레드를 생성해서 멀티스레드로 동작한다.
이때 트랜잭션이 걸려있었기 때문에 추가적으로 생성된 여러개의 스레드들은 서로 다른 1차 캐시를 지니고 있으며 이로인해서 조회를 정상적으로 할 수 없었던 것이다.
쉽게 말해 다른 스레드들은 저 testUser가 없었던 것이다.
테스트 자체에 대한 트랜잭션 어노테이션 때문에 아직 testUser가 기록되지 않은 형태로 추정된다.
@BeforeEach를 통해서 userRepository.save를 통해서 모든 INSERT 쿼리는 전부 날아갔다.
하지만 executorService부터는 각각 다른 스레드들이 다른 영속성 컨텍스트를 지니고 있으므로 조회 자체가 불가능해진 것이다.
최초의 main 스레드에서는 INSERT 쿼리가 전부 정상적으로 날아가서 testUsers를 지니고있다.
하지만 트랜잭션에 걸려있기 때문에 커밋이 되지 않는다.
그래서 다른 thread1, 2, 3 … 들은 SELECT 쿼리를 날려도 조회할 수 없다.
최종적으로 트랜잭션을 걸어서 트랜잭션이 종료될 때 커밋을 하는 것이 아니라 executorService로 다른 스레드들이 트랜잭션을 시작하기 전에 커밋을 통해 값을 넣어두면 된다.
단일 스레드로만 생각하다가 동시성 제어에 대해 테스트 코드를 작성하면서 executorService를 통해 멀티 스레드 환경에 대해 많은 고통을 겪어보았다.
다른 스레드들이 생겨났을 때 해당 스레드들은 영속성 컨텍스트를 공유하지못하고 전혀 다른 클라이언트라고 부류하면 될 것 같다.