이전 상황 정리
이전 글은 Data JPA를 사용해서 Delete를 진행했을 때, N+1이 발생해서, 성능 저하가 발생했었고, `delete where in ...` 쿼리를 직접 작성하는 것으로 엄청난 성능 향상을 가져왔었다.
이젠 이 쿼리를 프로젝트에 적용하고 성능을 체크해보자.
적용
프로젝트에 적용하기에 앞서, 다시 한번 관계를 확인하겠습니다. Feed를 삭제하면 다음과 같은 관계를 갖습니다.
삭제 성능을 확인하기 위해, 테스트 데이터를 집어넣고 삭제를 진행해보자.
테스트 코드가 상당히 길어서 대충 정리하면 Feed는 10개, Comment는 각 Feed당 100개, Reply는 각 Comment당 100개, ReplyLike는 각 Reply당 10개씩 있어서, ReplyLike가 총 100만개정도 있다고 보면 된다.
Data JPA의 Delete와 Delete In 쿼리를 사용해서 삭제를 진행해보자.
먼저 Data JPA의 Delete 쿼리이다.
9251ms... 데이터가 많다고는 하지만 9초나 걸리는건 사용자 입장에서 상당히 불편한 시간이 된다고 생각한다.
그럼 Delete In 쿼리의 시간을 측정해보자.
음..?
75313ms... 로그의 시간을 봐도 알 수 있지만 거의 12분이 걸렸다. 진짜 결과를 기다리다가 잘 뻔 했다.
왜 이전 테스트 코드에서는 잘 돌아갔는데, 안되는거지..??
테스트
뭔가 안된다는 사실은 알았다. 그럼 왜 안되는 걸까??
`where in ( ... )` 에서 괄호 안에 데이터가 많아서 그런게 아닐까?? 라는 생각을 해봤고, 테스트를 다시 돌려보기로 했다.
아래와 같은 코드로 변경해 in 절의 개수를 늘린 상태에서 삭제 쿼리를 날려봤다.
@BeforeEach
void beforeEach() {
for (int i = 0; i < 10_000; i++) {
Team team = new Team("팀" + i);
em.persist(team);
for (int j = 0; j < 10; j++) {
Member member = new Member("회원" + j, 25);
member.associatedWithTeam(team);
em.persist(member);
}
}
em.flush();
em.clear();
}
그 결과는 예상한 대로 나타났다.
casacde를 활용한 삭제의 경우 736ms의 결과가 나왔고, where in 절을 사용한 경우 2884ms가 나왔다.
결국 delete where in 방법은 제한된 상황에서만 사용될 수 있다는 점을 알게 되었다.
하지만 너무 느리다고 생각이 되어서 더 빠른 방법을 고민하던 중, `JDBC`에서 제공하는 Bulk 쿼리를 이용하면 좋겠다는 생각이 들었다. 스프링에서 제공하는 Spring JDBC 라이브러리의 `JdbcTemplate`을 사용해서 테스트를 해보기로 했다. 처음 delete in을 도입한 것만큼 엄청난 성능 차이가 발생하지는 않았지만, cascade delete의 736ms보다는 확실히 빠른 495ms로 33% 정도 감소시킬 수 있었다.
@Test
void deleteJdbcTemplateTest() throws Exception{
long start = System.currentTimeMillis();
List<Integer> teamIds = jdbcTemplate.queryForList("select t.id from Team t", Integer.class);
jdbcTemplate.batchUpdate(
"delete from member m where m.team_id = ? ", new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {preparedStatement.setLong(1, teamIds.get(i));}
@Override
public int getBatchSize() {return teamIds.size();}
});
jdbcTemplate.batchUpdate("delete from Team t where t.id = ? ", new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {preparedStatement.setLong(1, teamIds.get(i));}
@Override public int getBatchSize() {return teamIds.size();}
});
long end = System.currentTimeMillis();
logPerf(log, "deleteJdbcTemplateTest", start, end); // [deleteJdbcTemplateTest] cost 495ms
}
JdbcTemplate을 적용해서 그럼 테스트를 진행해보자.
마찬가지의 케이스인 대략 100만건에 대한 삭제 상황에 대해서 9251ms 에서 8765ms로 대략 5% 정도 감소했다. 앞선 테스트에 비해서는 큰 개선은 아니지만, 해당 상황이 여러번 발생한다면 의미있는 수치라고 생각했다.
결론
지금까지 JPA에서 사용하는 Cascade를 사용해 삭제를 했을 때, 발생했던 문제와 이를 개선할 수 있는 방법에 대해서 알아보았다. 각 방법에 대한 특징을 알아볼 수 있었고, 적합한 상황에 따라서 방법을 선택하면 좋을 것 같다. 무엇보다 JPA만을 고집하지 말고, 다른 방법을 선택할 수 있다면, 그것을 고려하는 것도 좋다고 생각이 들었다.