숙박 예약 서비스 개발 중 인벤토리 추가 API를 개발하고 있었습니다.
숙소 관리자는 본인이 등록한 RoomType의 Inventory를 추가할 수 있습니다.
아래 Json 데이터를 받아 startDate 에서 endDate까지 예약 가능한 Room 갯수를 availableCount만큼 추가하게됩니다.
{
"startDate": LocalDate,
"endDate": LocalDate,
"availableCount" : Integer
}
@Service
@RequiredArgsConstructor
@Transactional
public class RoomInventoryService {
private final RoomTypeService roomTypeService;
private final RoomInventoryRepository roomInventoryRepository;
public void addInventory(
long roomTypeId, RoomInventoryAddRequestDto roomInventoryAddRequestDto, AuthUser loginUser
) {
RoomType roomType =
roomTypeService.getRoomTypeById(roomTypeId);
VerificationUtils.verifyOwnerPermission(loginUser, roomType.getOwnerId());
LocalDate startDate = roomInventoryAddRequestDto.getStartDate();
LocalDate endDate = roomInventoryAddRequestDto.getEndDate();
List<LocalDate> dates = Stream.iterate(startDate, date -> date.plusDays(1))
.limit(ChronoUnit.DAYS.between(startDate, endDate.plusDays(1)))
.collect(Collectors.toList());
dates.forEach(date -> {
RoomInventory instance =
RoomInventory.createInstance(roomType, date, roomInventoryAddRequestDto.getAvailableCount());
roomInventoryRepository.save(instance);
});
}
}
실질적으로 Inventory를 추가하는 Service 레이어 부분입니다.
해당 코드에서는 startDate에서 endDate까지 반복문을 돌며 save() 메서드를 호출합니다.
startDate가 "2021-10-01", endDate가 "2021-10-20"이라면 20개의 개별적인 INSERT 쿼리를 날리게 됩니다.
이때 쿼리를 던지고 응답받은 후에야 다음 쿼리를 전달하기 때문에 지연이 발생하게 됩니다.
임시 방편으로 최대 30일 기간의 인벤토리를 추가할 수 있다는 제약조건을 추가하였지만 좀 더 근본적인 문제를 해결하려합니다.
List<RoomInventory> roomInventoryList = new ArrayList<>();
dates.forEach(date -> {
RoomInventory instance =
RoomInventory.createInstance(roomType, date, roomInventoryAddRequestDto.getAvailableCount());
roomInventoryList.add(instance);
});
roomInventoryRepository.saveAll(roomInventoryList);
saveAll() 메서드를 통해 JPA Batch Insert를 하려하였으나 실패하였습니다. 이전 포스팅에서 살펴보았듯이 현재 @GeneratedValue(strategy = GenerationType.IDENTITY)
를 사용하고 있는데 이 경우 JPA Batch Insert를 할 수 없다고 합니다.
DB에 Insert가 되어야 id값을 알 수 있기 때문에 쓰기 지연이 아닌 즉각적으로 쿼리를 날릴 수 밖에 없기 때문입니다.(1차 캐시에 쌓아둘 수 없습니다)
다른 방안으로 Spring JDBC를 이용하여 Batch Insert를 진행하도록 하겠습니다. 현재 spring-boot-starter-data-jpa 라이브러리를 사용하고 있는데 이는 spring-jdbc 라이브러리 의존성을 갖고 있기 때문에 추가적인 라이브러리 없이 적용할 수 있었습니다.
@Repository
@RequiredArgsConstructor
public class JdbcRoomInventoryRepository {
private final JdbcTemplate jdbcTemplate;
public void batchInsertRoomInventories(List<RoomInventory> roomInventories) {
String sql = "INSERT INTO room_inventory "
+ "(roomtype_id, inventory_date, available_count, created_at, modified_at) VALUES (?, ?, ?, ?, ?)";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
RoomInventory roomInventory = roomInventories.get(i);
ps.setLong(1, roomInventory.getRoomType().getId());
ps.setObject(2, roomInventory.getInventoryDate());
ps.setInt(3, roomInventory.getAvailableCount());
ps.setObject(4, LocalDateTime.now());
ps.setObject(5, LocalDateTime.now());
}
@Override
public int getBatchSize() {
return roomInventories.size();
}
});
}
}
JdbcTemplate의 batchUpdate() 메서드를 통해 Batch insert를 진행하도록 하겠습니다.
APM을 확인해본 결과 다수의 INSERT 쿼리가 나가는 것이 아닌 하나의 INSERT 쿼리가 나가는 것을 확인할 수 있었습니다.
addInventory() 메서드 수행 시간을 측정해본 결과 아래와 같은 결과를 얻을 수 있었습니다.
20개 데이터 INSERT 수행 시 1056 ms → 368 ms
90개 데이터 INSERT 수행 시 2880 ms → 490 ms
https://kapentaz.github.io/jpa/JPA-Batch-Insert-with-MySQL/#
https://wave1994.tistory.com/160
https://javabydeveloper.com/spring-jdbctemplate-batch-update-with-maxperformance/