자바생
article thumbnail
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
profile

자바생

@자바생

틀린 부분이 있다면 댓글 부탁드립니다~😀

검색 태그