테스트 코드 작성
테스트 방법 선택
동시성 제어 테스트를 위해 테스트 코드작성을 하려고 계획을 했고 가장 처음 만난 문제는
동시성 제어는 단위테스트로 작성해야 하는가 통합테스트로 작성해야 하는가? 에 고민에 빠졌고
동시성 제어는 멀티스레드 환경에서 테스트를 진행해야하고 단위테스트는 각 메서드나
함수 단위로 테스트를 해야하기때문에 통합 테스트로 진행하기로 했다.
테스트 흐름
아래의 순서대로 로직을 작성했고 동시성제어는 성공적으로 작동했다.
하지만 문제점이 발생했다.
- 객체를 생성하여 실제 DB에 저장을 한다
- 저장 된 객체를 모두 불러온다.
- 동시성 제어 테스트를 위해 동시에 서비스 로직을 호출한다
- 입차가 성공한 수와 입차가 실패한 수를 모두 저장한다
- 입차 성공수와 입차 실패수가 모두 맞는지 검증한다
1차 문제점
만난 문제점은 다음과 같다
- 테스트 코드에 Transectional을 적용하면 롤백이 된다고 알고있었지만 동시성 제어 테스트 후
동시성 제어를 테스트한 모든 자료가 DB에 저장되어있었다.
문제점을 멘토님께 질의
Q. 예약, 입차 로직에서 동시성 제어를 테스트 해볼 테스트 코드를 작성했습니다.
테스트에서 동시성 제어는 문제없이 테스트가 잘 되었으나 테스트 후 실제 DB에
자료가 입력되었는데 이 방법이 맞는건지 아니면 테스트 방법이 잘못되었는지 궁금합니다.
A. 테스트 코드 실행 시 H2 DB를 사용하여 테스트를 진행하면 실제 DB에 입력이 되지않고
테스트는 가능하니 해당 방향으로 진행해보자
2차 테스트 작성
테스트 DB를 사용하기위해서 테스트 실행시마다 테스트 DB로 작동되는 방법을 찾아봤다.
@ActiveProfiles("test")
어노테이션을 사용하면 아주 간단하게 설정이 가능했다.
application.yml
spring:
config:
activate:
on-profile: "test"
h2:
console:
enabled: true
jpa:
show-sql: true
database: H2
hibernate:
ddl-auto: create
properties:
hibernate:
format_sql: true
show_sql: true
datasource:
url: jdbc:h2:mem:db
username: sa
password:
2차 문제점
H2 DB로 데이터를 호출하니 전에 몰랐던 문제점들이 더 많이 발생했다.
- 객체를 생성하여 Save 하였는데 서비스 로직에서 findById를 했을때 결과가 없다.
⇒ 1차 흐름에서 내가 생성한 객체를 가져온줄 알았지만 사실 실제 DB의 데이터가 있어서
호출이 되었던것이다
( 테스트로직에선 findById를 했을때 정상적으로 객체를 호출한다 ) - 다른 조에서는 테스트 코드 작성 시 롤백이 정상적으로 수행된다고 하였고
Junit4를 사용했다고 하기에 동일하게 Junit4를 사용해봤지만 적용되지않았다.
해결 시도
- save 후 flush로 저장하여 즉시 데이터를 찾아올 수 있도록 시도해봤지만 실패.
@beforeEach
어노테이션을 적용하고 객체를 따로 먼저 생성하고 테스트 메서드를 호출하게 적용해봤지만 실패.@Transectional
이 어떤이유에서든 롤백이 되어 저장이 되었어도 롤백이 되었을까 싶은 생각을 하였고@Rollback
어노테이션을 적용해봤지만 실패@Commit
어노테이션은 바로 commit 적용된다고하여 적용해봤지만 실패.
해결
테스트 코드의 @Transactional
을 모두 삭제 후 테스트 코드가 동시성 제어까지 완벽하게
진행되었다..! 사실 테스트가 성공한 이유를 아직도 잘 모르겠고 몇가지 의문점이 있는데
조금 더 생각을 해봐야할것같다
의문점
- 왜
@Transactional
을 테스트 코드에서 제외하니 동작을 하는가?- 추측 : 서비스 로직의 Transaction과 테스트코드의 Transaction이 별개로 작동을 한다면.
Transactional을 메서드 전체에 적용을 하면 save가 일어나도 transactional이 종료될때까지 캐시에 저장되어있어 서비스 로직을 호출해도 findById를 했을때 찾을수 없지않을까?
- 추측 : 서비스 로직의 Transaction과 테스트코드의 Transaction이 별개로 작동을 한다면.
@Transactional
을 제외했는데 더티체킹이 일어날 수 없는데 왜 객체의 update가
작동이 되는가?- 추측 : 아직 repository에 Save한 상태가 아니기때문에 캐시에 저장되어있어서 update하면 즉시 반영이 된다.
성공한 테스트 코드
@ActiveProfiles("test")
@Slf4j
@SpringBootTest
class BookingServiceTest {
@Autowired
private BookingService bookingService;
@Autowired
private ParkInfoRepository parkInfoRepository;
@Autowired
private ParkOperInfoRepository parkOperInfoRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private CarRepository carRepository;
@Autowired
private MgtService mgtService;
@Test
void bookingPark() throws InterruptedException {
//when
ParkInfo parkInfo = parkInfoRepository.save(ParkInfo.of("테스트주차장", "테스트주소1", "테스트주소2", "33.2501489768202", "126.563230508718"));
ParkOperInfo parkOperInfoTmp = ParkOperInfo.of(parkInfo, "민영");
parkOperInfoTmp.update("00:00", "23:59","00:00", "23:59","00:00", "23:59",30, 1000, 30, 500, 20);
parkOperInfoRepository.save(parkOperInfoTmp);
int numOfUsers = 20;
// 대기하는 스레드의 숫자를 지정
CountDownLatch endLatch = new CountDownLatch(numOfUsers);
ExecutorService executorService = Executors.newFixedThreadPool(numOfUsers);
AtomicInteger successCount = new AtomicInteger();
AtomicInteger failCount = new AtomicInteger();
ParkOperInfo parkOperInfo = parkOperInfoRepository.findByParkInfoId(1L).get();
createUser(numOfUsers);
ParkSpaceInfo parkSpaceInfo = mgtService.getParkSpaceInfo(parkOperInfo);
ParkSpaceInfo useSpaceInfo = mgtService.getUseSpaceInfo(parkInfo);
BookingInfoRequestDto bookingInfoRequestDto = new BookingInfoRequestDto();
bookingInfoRequestDto.setStartDate(LocalDateTime.now());
bookingInfoRequestDto.setEndDate(LocalDateTime.now().plusHours(1));
List<User> userList = new ArrayList<>();
for (long i = 1; i <= numOfUsers; i++) {
userList.add(userRepository.findById(i).get());
}
//given
for (User user: userList){
executorService.execute(() -> {
try {
System.out.println("!--- 6번 ---!");
bookingService.bookingPark(parkInfo.getId(),bookingInfoRequestDto,user);
successCount.getAndIncrement();
log.info("예약 성공");
log.info("예약유저 : {}", user.getUserId());
} catch (CustomException e) {
failCount.getAndIncrement();
log.info(e.getMessage());
log.info("예약 자리가 꽉찼습니다.");
}
//스레드가 끝나면 -1 을 해서 endLatch를 1 줄임.
endLatch.countDown();
});
}
// Then
log.info("enter 동시성 테스트 결과 검증");
// 모든 스레드들이 끝날때까지 대기. 모든 스레드들이 끝나면 다음 코드 실행, 즉 endLatch가 0이 되면 다음 코드 실행
endLatch.await();
log.info("예약 성공 개수: {}", successCount.get());
Assertions.assertEquals(parkSpaceInfo.getBookingCarSpace()-useSpaceInfo.getBookingCarSpace(), successCount.get());
log.info("예약 실패 개수: {}", failCount.get());
Assertions.assertEquals(numOfUsers-parkSpaceInfo.getBookingCarSpace()-useSpaceInfo.getBookingCarSpace(), failCount.get());
}
private void createUser(int count){
List<User> userList = new ArrayList<>();
List<Car> carList = new ArrayList<>();
for (int i = 0; i < count; i++) {
User user = User.of("user"+i,"1234");
userList.add(user);
carList.add(Car.of("11가1"+String.format("%03d", i + 1),user,true ));
}
userRepository.saveAll(userList);
carRepository.saveAll(carList);
}
}
해결중인 의문점
- 왜
@Transactional
을 테스트 코드에서 제외하니 동작을 하는가?
코드를 실행하면서 진행되는 로그를 확인하기위해 사이사이 로그를 찍어놓고 확인하던 중
유의미한 차이를 발견했다.
List<User> userList = new ArrayList<>();
for (long i = 1; i <= numOfUsers; i++) {
userList.add(userRepository.findById(i).get());
}
System.out.println("!--- 5번 ---!");
//given
for (User user: userList){
executorService.execute(() -> {
try {
bookingService.bookingPark(parkInfo.getId(),bookingInfoRequestDto,user);
successCount.getAndIncrement();
log.info("예약 성공");
log.info("예약유저 : {}", user.getUserId());
} catch (CustomException e) {
failCount.getAndIncrement();
log.info(e.getMessage());
log.info("예약 자리가 꽉찼습니다.");
}
//스레드가 끝나면 -1 을 해서 endLatch를 1 줄임.
endLatch.countDown();
});
}
위의 코드의 로그찍은 위치에 나온 내용이다
- 트랜잭션을 걸지 않았을때
- 트랜젝션을 걸었을때
위의 로그에서 확인가능하듯 트랜잭션을 걸지않으면 findById를 했을때 직접 쿼리를 날리게 된다.
하지만 아래의 로그에서는 영속성컨텍스트에서 바로 가져오게 된다.
@Transactional
을 안걸었을때는 로그찍은 위치에서는
이미 객체가 Commit이 되었다는 의미로 생각이 된다.
@Transactional
을 걸었을때는 save로직이 실행 될때까지도 영속성 컨텍스트 안에있는 상태이고
Commit이 진행되지 않았다는것을 의미하는 것으로 생각이 된다
그렇기 때문에 @Transactional
어노테이션을 달았을경우
즉 @Transactional
안에서 생성한 객체는 바로 서비스 로직에 사용할수없다. 라는 결론을 혼자내려봤지만 조금 더 많은 정보를 찾아봐야 할것같다.
빌드 시 에러발생
빌드 시 에러가 발생하여 에러를 확인해본 결과
테스트 코드를 실행하면서 관리자가 아니라는 Exception을 발생시켰다
즉 실제 DB를 조회하여 데이터를 가져온다는것으로 보인다
그렇다면 빌드를 진행할때 dev profile을 읽어온다는 것인데
build를 진행할때도 Test코드에 대해서는 test profile을 가져오는 방법을 찾아보게 되었다.
시도
- 빌드 할때도 테스트 profile을 참조하도록 build.gradle에 추가해봤지만 실패.
test {
systemProperty 'spring.profiles.active', 'test'
}
bootRun {
systemProperty "spring.profiles.active", "test"
}
@SpringBootTest(properties = "spring.profiles.active:test")
SpringBootTest 어노테이션을 불러올때 적용시켜봤지만 실패.@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2)
어노테이션을 적용하고 테스트 해봤지만 실패.
실마리 발견
위의 방법을 여러방면으로 시도해봐도 해결 될 기미가 보이지않아
서비스 로직에 로그를 찍기 시작했는데 이상한 부분이 보였다.
분명 H2 DB를 생성했고 User의 ID는 1번부터 시작해야되는데
무슨 문제인지 2번부터 시작한다 즉 ParkInfo가 2번이 생성되었다는 이야기가 되고
코드에서 2번 주차장을 찾게 변경을 했는데 성공적으로 빌드가 완료되었다
해결
- 빌드를 진행하면서 MgtServiceTest보다 먼저 진행이되는 BookingServiceTest에서 주차장 정보를 생성한다 하지만 코드에서
@Transectional
을 사용하지 않기때문에 Rollback이 일어나지 않고 다음 테스트에 영향을 미치게 된것이다. - 기존에 1L로 하드코딩 되어있던 ParkInfoId를 변수에 담아 ParkInfoId가 다른 숫자가 되어도 테스트가 가능하게 변경한 뒤로 정상적으로 빌드가 되었다!
- 테스트가 서로에게 영향을 미칠수 있다는점과 하드코딩에서 나올수 있는 문제를 알게되었다.
'공부 > 성능개선' 카테고리의 다른 글
[NCP] Load Balancer 적용 (1) | 2024.08.13 |
---|---|
[ParkNav] QueryDSL을 이용한 관리자 페이지 성능개선 (0) | 2023.04.27 |
[ParkNav] 예약처리 일관성 테스트 (0) | 2023.04.27 |
[ParkNav] 알고리즘 Version 0 ~ Version 2 (0) | 2023.04.27 |
[ParkNav] QueryDSL을 이용한 성능개선 (0) | 2023.04.19 |
댓글