JPA 성능 개선기 4. saveAll()
우아한테크코스

JPA 성능 개선기 4. saveAll()

상황

현재 스케줄을 등록할 때 기존 스케줄을 deleteAll한 뒤, 새로운 스케줄을 saveAll하는 방식을 취하고 있다.
기존에는 하루씩 일정을 등록할 수 있었으나, 사용자의 피드백을 반영하는 과정에서 다중선택 기능이 추가되었다.
따라서 여러 날의 일정을 한번에 등록할 수 있게 되었다.

문제점

여러 스케줄을 등록할 때 스케줄 하나하나마다 save 쿼리가 나가 DB에 부하를 주고 있는 상황이다.

코드

    private void saveAllByCoachAndDate(Long coachId, ScheduleUpdateRequest request) {
        Coach coach = findCoach(coachId);
        List<Schedule> schedules = toSchedules(request, coach);
        scheduleRepository.saveAll(schedules); // 이 부분에서 성능 문제가 발생하고 있었다!
    }

sql 쿼리문

해결방안

JdbcTemplate 사용

JPA로는 saveAll 호출 시 쿼리가 하나하나 발생하는 문제를 해결할 수 없었다.
왜냐하면 JPA에서는 엔티티를 저장하면 그 엔티티를 영속성컨텍스트에 저장하기 위해 식별자가 필요하기 때문이다.
그래서 saveAll을 사용하더라도 Batch Insert가 아닌, 엔티티 하나하나 쿼리문을 날려줄 수 밖에 없다.

이 문제를 해결하기 위해서는 JPA가 아닌 JdbcTemplate을 사용해야만 한다.

DAO

    @RequiredArgsConstructor
    @Repository
    public class ScheduleDao {

        private static final int BATCH_SIZE = 1000;

        private final JdbcTemplate jdbcTemplate;

        public void saveAll(List<Schedule> schedules) {
            int batchCount = 0;
            List<Schedule> subItems = new ArrayList<>();
            for (int i = 0; i < schedules.size(); i++) {
                subItems.add(schedules.get(i));
                if ((i + 1) % BATCH_SIZE == 0) {
                    batchCount = batchInsert(batchCount, subItems);
                }
            }
            if (!subItems.isEmpty()) {
                batchCount = batchInsert(batchCount, subItems);
            }
        }

        private int batchInsert(int batchCount, List<Schedule> subItems) {
            jdbcTemplate.batchUpdate("INSERT INTO schedule (coach_id, local_date_time, is_possible) VALUES (?, ?, ?)",
                    new BatchPreparedStatementSetter() {
                        @Override
                        public void setValues(PreparedStatement ps, int i) throws SQLException {
                            ps.setLong(1, subItems.get(i).getCoach().getId());
                            ps.setTimestamp(2, Timestamp.valueOf(subItems.get(i).getLocalDateTime()));
                            ps.setBoolean(3, subItems.get(i).getIsPossible());
                        }

                        @Override
                        public int getBatchSize() {
                            return subItems.size();
                        }
                    });
            subItems.clear();
            batchCount++;
            return batchCount;
        }
    }

추가로 생각해볼 점

JPA와 JdbcTemplate을 함께 사용할 때 주의할 점

JPA의 구조적인 한계 때문에 saveAll() 로직에서 JdbcTemplate을 사용할 수 밖에 없었다.
자연스레 JPA와 JdbcTemplate을 함께 사용하게 되었다. 하나의 WAS에서 JPA와 JdbcTemplate을 함께 사용할 때 주의해야 할 점은 무엇일까?

만약 JdbcTemplate을 조회 로직에서도 사용한다면 문제가 발생할 수 있다.
왜냐하면 JdbcTemplate은 영속성 컨텍스트를 거치지 않고 바로 DB를 조회하기 때문에 데이터 정합성에 문제가 발생할 수 있기 떄문이다. 그래서 JdbcTemplate을 조회시 사용한다면 JdbcTemplate을 사용하기 전에 영속성 컨텍스트를 flush해서 데이터의 정합성을 보장해야 한다.

JdbcTemplate 사용 전 flush를 AOP로 처리하는 방법도 추후에 고민해봐야겠다.

출처