728x90
글을 쓰게 된 이유
YAPP 프로젝트 중 Account 삭제 기능을 맡았습니다. ( 토이 프로젝트이기 때문에 hard delete 진행)
Account는 Club, Comment, Pet 등 대부분의 Entity와 연관관계를 맺고 있기 때문에 벌크 삭제를 했어야했습니다
그래서 어떻게 하면 최적화 된 벌크 삭제를 할 수 있을까라는 생각을 하게 됐습니다
이를 계기로 제가 알고 있는 delete 방법에 대해 정리해보고자 이 글을 작성했습니다
예제 코드
@Getter
@NoArgsConstructor
@ToString(of = {"id", "username", "age"})
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
@Column(nullable = false)
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
public Member(String username) {
this.username = username;
}
public Member(String username, Team team) {
this.username = username;
if (team != null) {
changeTeam(team);
}
}
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team", orphanRemoval = true, cascade = CascadeType.ALL)
private List<Member> members = new ArrayList<>();
public Team(String name) {
this.name = name;
}
}
테스트 데이터
@BeforeEach
public void setUp() {
for (int i = 1; i <= 3; i++) {
Team team = new Team("team" + i);
for (int j = 1; j <= 5; j++) {
Member member = new Member("member" + j);
member.changeTeam(team);
}
teamRepository.save(team);
}
}
cascade DELETE
@Test
@DisplayName("cascade를 이용하여 Member 삭제")
void cascadeDelete() throws Exception {
//given
//when
teamRepository.deleteById(1L);
//then
assertEquals(teamRepository.findAll().size(), 2);
}
- 현재 Team의 members는 Cascade DELETE가 포함된 ALL로 되어있습니다
- 그래서 Team Entity를 삭제할 때, 그 안에 포함되어 있는 member의 수만큼 delete 쿼리가 나가게 됩니다
- 사진과 같이 총 6번의 쿼리(5번의 member delete, 1번의 team delete)
- 1개의 Team에 10만명의 member가 있다고 하면, delete 쿼리가 10만번 나가게 됩니다
- 그렇다면 delete 쿼리를 한번에 날릴 순 없을까요?
deleteAllInBatch
- Spring Data JPA에서 제공하는 deleteAllInBatch를 사용하여 Member를 삭제해보겠습니다
@Test
@DisplayName("deleteAllInBatch를 이용하여 Member 삭제")
void deleteBatch() throws Exception {
//given
Team savedTeam = teamRepository.getById(1L);
//when
memberRepository.deleteAllInBatch(savedTeam.getMembers());
//then
assertEquals(memberRepository.findAll().size(), 10);
}
- 원하는 대로 delete 쿼리가 한개만 나가게 됐습니다
- 그렇다면 여기서 deleteAllInBatch 메서드는 어떻게 작동하길래 한번에 삭제할 수 있을까요?
deleteAllInBatch 코드
Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs first level cache and the database out of sync. Consider flushing the EntityManager before calling this method.
- 지정된 엔티티를 “일괄 삭제"한다고 합니다
- 하지만 JPA의 first level cache와 데이터베이스를 동기화하지 않은 상태로 유지하기 때문에 메서드를 호출하기 전에 EntityManager를 비우라고 한다,, 왜 그럴까요?
- 내부 코드를 한번 봐봅시다
@Override
@Transactional
public void deleteAllInBatch(Iterable<T> entities) {
Assert.notNull(entities, "Entities must not be null!");
if (!entities.iterator().hasNext()) {
return;
}
applyAndBind(getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()), entities, em)
.executeUpdate(); //요 부분
}
- 마지막에 보면 executeUpdate() 메서드를 볼 수 있습니다
- 우리는 Spring data JPA에서 벌크 연산을 공부할 때 많이 봤던 메서드입니다
- 만약 해당 메서드를 보고 잘 모르겠다하시면 블로그를 참고하시길 바랍니다
- 간단히 설명드리자면 벌크 연산에 사용하는 메서드이고, 데이터베이스에 직접적으로 데이터를 변경하기 때문에 flush를 하지 않는다면, 변경 값이 반영되지 않을 수 있기 때문에 EntityManager를 비우라고 권장합니다
deleteAllInBatch 단점
- 위 쿼리 사진을 보시면 여러 개의 or 절의 쿼리가 나가게 됩니다
- 하지만 MySQL에서 or 절과 in 절을 비교했을 때, in 절의 성능이 더욱 좋기 때문에 in 쿼리를 권장합니다
- 그래서 많은 데이터를 삭제할 때, delete 쿼리가 한 번 나가면서, 동시에 in 쿼리를 사용해봅시다
in절 직접 사용
public interface MemberRepository extends JpaRepository<Member, Long> {
@Modifying
@Transactional
@Query("delete from Member m where m.id in :ids")
long deleteByIdInQuery(@Param("ids") List<Long> ids);
}
- delete 쿼리가 한번에 나가는 것을 알 수 있습니다
결론
- 1 : N의 관계에서 1 쪽의 데이터를 대량 삭제할 때, CasCade Delete 를 사용하면 매우 많은 쿼리가 나가게 된다
- deleteAllInBatch는 or 을 사용하여 Entity를 삭제한다
- IN 절을 사용하여 벌크 삭제를 수행한다
- 따라서 1 : N 관계에서 1 쪽의 데이터를 대량 삭제할 때, 연관된 N 데이터를 IN 절로 삭제한 뒤, 1 도 똑같은 과정으로 삭제하는 것이 좋다
728x90
'Spring Data' 카테고리의 다른 글
@NotNull vs @Column(nullable = false) (0) | 2022.10.22 |
---|---|
Find vs Get (0) | 2022.09.25 |
JpaRepository vs CrudRepository (0) | 2022.08.23 |
[YAPP] 검색 성능 최적화 일지(Feat. Elasticsearch) (0) | 2022.07.08 |
Querydsl의 like vs contains (0) | 2022.05.22 |