본문 바로가기
테스트

[Trasactional] 자바 스케줄러 테스트 시도 과정

by 순원이 2024. 3. 28.

요구사항: 게임이 시작하면 Room 상태를 PROGRESS로 바꾸고, 1분 뒤에 FINISH로 바꿔라

 

서비스단 코드

@Service
@RequiredArgsConstructor
public class RoomUpdateService {

    private final UserRepository userRepository;
    private final UserRoomRepository userRoomRepository;
    private final RoomRepository roomRepository;
    private final UserService userService;
    private final RoomService roomService;
    private final SchedulerService schedulerService;


    @Transactional
    public void gameStart(Integer roomId, GameStartRequest request) {
        Room room = roomRepository.findById(roomId).orElseThrow(CustomException::new);
        User user = userRepository.findById(request.getUserId()).orElseThrow(CustomException::new);

        user.isUserHost(room);
        roomService.validateRoomIsFull(room);
        room.validateWaitStatus();

        room.updateRoomStatus(RoomStatus.PROGRESS);
        schedulerService.scheduleRoomStateChangeToFinish(roomId);
    }
}

 

스케쥴러 코드

@Service
public class SchedulerService {

    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    private final RoomRepository roomRepository;

    @Autowired
    public SchedulerService(RoomRepository roomRepository) {
        this.roomRepository = roomRepository;
    }

    public void scheduleRoomStateChangeToFinish(Integer roomId) {
        scheduler.schedule(() -> {
            changeRoomStateToFinish(roomId);
        }, 1, TimeUnit.MINUTES);
    }

    public void changeRoomStateToFinish(Integer roomId) {
        Room room = roomRepository.findById(roomId).orElseThrow(CustomException::new);
        room.updateRoomStatus(RoomStatus.FINISH);
        System.out.println("스케쥴러 성공!!!!!!!!!!!!!!!!!!!!!!!!!1");
    }
}

 

처음  테스트 코드

@SpringBootTest
@AutoConfigureMockMvc
class RoomUpdateApiTest {

    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;
    @Autowired
    UserRepository userRepository;
    @Autowired
    RoomRepository roomRepository;
    @Autowired
    UserRoomRepository userRoomRepository;

    @PersistenceContext
    private EntityManager entityManager;


    @Test
    @Transactional
    @DisplayName("게임 시작 API 통합 테스트")
    void gameStart() throws Exception{

        User user =  User.builder().userStatus(UserStatus.ACTIVE).build();
        User user2 =  User.builder().userStatus(UserStatus.ACTIVE).build();
        userRepository.save(user);
        userRepository.save(user2);

        Room room = Room.builder().host(user).roomType(RoomType.SINGLE).roomStatus(RoomStatus.WAIT).build();
        roomRepository.save(room);

        UserRoom userRoom = UserRoom.builder().user(user).room(room).build();
        UserRoom userRoom2 = UserRoom.builder().user(user2).room(room).build();
        userRoomRepository.save(userRoom);
        userRoomRepository.save(userRoom2);

        GameStartRequest gameStartRequest = new GameStartRequest();
        gameStartRequest.setUserId(user.getId());

        String jsonRequest = this.objectMapper.writeValueAsString(gameStartRequest);

        mockMvc.perform(put("/room/start/" + room.getId())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(jsonRequest))
                .andExpect(status().isOk())
                .andDo(print());


        Thread.sleep(70000); // 1분 10초 대기

        clearCache();
        // 데이터베이스에서 방 상태를 다시 조회
        Room updatedRoom = roomRepository.findById(room.getId()).orElseThrow(CustomException::new);

        // 방 상태가 FINISH로 변경되었는지 확인
        assertEquals(RoomStatus.FINISH, room.getRoomStatus());
    }
    public void clearCache() {
        entityManager.clear();
    }
}

 

테스트 코드 설명

데이터베이스는 h2를 사용했다.
호출에 필요한 User, Room, UserRoom을 데이터베이스에 저장한다.
mockMvc로 호출한다.
1분 10초 대기 후 데이터베이스에서 Room을 조회해 상태를 확인 한다.

 

결과

결과는 실패했다. 응답은 잘 왔으나 
스케쥴러 성공!!!!!!!!!!!!!!!!!!!!!!!!!1 라는 문구가 출력이 안 되었다.

 

두 번째 시도

테스트 코드 말고 Postman을 이용해 직접 호출해 보겠다.
데이터베이스는 mysql을 사용했고 api호출에 필요한 데이터들은 직접 넣어줬다.
사진에 나와있는 것처럼 스케쥴러 성공!!!!!!!!!!!!!!!!!!!!!!!!!1 문구는 출력되었지만 Room Status는 변하지 않았다.

 

 

JPA의 더티체킹이 안될 수도 있다는 의심이 들어 save() 함수를 집어넣었다.(+ 1분 뒤에 호출됐나 알기 위해서 System함수에 LocalDateTime.now() 추가)

 

두 번째 시도 코드

 public void changeRoomStateToFinish(Integer roomId) {
        Room room = roomRepository.findById(roomId).orElseThrow(CustomException::new);
        room.updateRoomStatus(RoomStatus.FINISH);
        roomRepository.save(room);
        System.out.println("스케쥴러 성공!!!!!!!!!!!!!!!!!!!!!!!!!1" + LocalDateTime.now());
    }

 

 

결과

성공!!! FINSH로 바꼈다.
그치만 @Transactional을 선언하고 save()를 빼면 더티체킹이 되나 궁금해서 실험해봤다

 

세 번째 시도 코드

 @Transactional
    public void changeRoomStateToFinish(Integer roomId) {
        Room room = roomRepository.findById(roomId).orElseThrow(CustomException::new);
        room.updateRoomStatus(RoomStatus.FINISH);
        System.out.println("스케쥴러 성공!!!!!!!!!!!!!!!!!!!!!!!!!1" + LocalDateTime.now());
    }

 

 

결과

@Transactional을 선언하면 더티채킹이 되지 않을까 했는데 결과는 실패했다. @Transactional을 이번에는 @Transactional도 살리고 save()도 살려보겠다.

 

네 번째 시도 코드

   @Transactional
    public void changeRoomStateToFinish(Integer roomId) {
        Room room = roomRepository.findById(roomId).orElseThrow(CustomException::new);
        room.updateRoomStatus(RoomStatus.FINISH);
        roomRepository.save(room);
        System.out.println("스케쥴러 성공!!!!!!!!!!!!!!!!!!!!!!!!!1" + LocalDateTime.now());
    }

 

결과

 

처음 @Transactional을 선언하지 않은 이유는 트랜잭션이 전파될까봐 선언하지 않았다. 스케쥴러는 1분 뒤에 실행하지만 서비스로직은 요청 오는 즉시 실행되어야 한다. 트랜잭션이 전파되면 1분뒤에 스케쥴러가 끝나야 데이터베이스에 저장되는 것을 우려했다.

 

1. Transactinal propagation에 대해서 공부해 봐야겠다.(sonaLint)

  • "changeRoomStateToFinish's" @Transactional requirement is incompatible with the one for this method.


2. 스케줄러를 포함한 api는 어떻게 테스트할지 공부해 봐야겠다