본문 바로가기
memo/기록

jpa 성능 최적화하기2 (update VS delete)

by 킹차니 2023. 3. 4.

이전 글과 같은 상황에서, Ask를 삭제한다고 해보자.

이때 AskImage, Comment는 cascade = ALL 로 지정되어 있어서 Ask를 JPA의 CrudRepository를 사용하여 삭제한다면 AskImage, Comment가 자동으로 모두 삭제된다.

@Entity
public class Ask{

	//... 중략

    @OneToMany(mappedBy = "ask", cascade = ALL)
    private List<AskImage> askImages = new ArrayList<>();

    @OneToMany(mappedBy = "ask", cascade = ALL)
    private List<Comment> comments = new ArrayList<>();
}

그런데 만약 Ask가 2개의 이미지를 가지고 있고, 3개의 코멘트를 가지고 있다면 2개의 이미지에 대한 delete 쿼리가 따로 나가고, 3개의 코멘트에 대한 delete 쿼리가 따로 나간다. 

 

실행해보면 아래와 같은 쿼리들이 나간다.

Hibernate: 
	select
        askimages0_.ask_id as ask_id6_1_0_,
        askimages0_.id as id1_1_0_,
        askimages0_.id as id1_1_1_,
        askimages0_.created_date as created_2_1_1_,
        askimages0_.is_enable as is_enabl3_1_1_,
        askimages0_.last_modified_date as last_mod4_1_1_,
        askimages0_.ask_id as ask_id6_1_1_,
        askimages0_.filename as filename5_1_1_ 
    from
        ask_image askimages0_ 
    where
        askimages0_.ask_id=?
Hibernate: 
    select
        commenttoa0_.ask_id as ask_id13_2_0_,
        commenttoa0_.comment_to_ask_id as comment_1_2_0_,
        commenttoa0_.comment_to_ask_id as comment_1_2_1_,
        commenttoa0_.created_date as created_2_2_1_,
        commenttoa0_.is_enable as is_enabl3_2_1_,
        commenttoa0_.last_modified_date as last_mod4_2_1_,
        commenttoa0_.content as content5_2_1_,
        commenttoa0_.garage_id as garage_12_2_1_,
        commenttoa0_.day as day6_2_1_,
        commenttoa0_.from_time as from_tim7_2_1_,
        commenttoa0_.month as month8_2_1_,
        commenttoa0_.to_time as to_time9_2_1_,
        commenttoa0_.year as year10_2_1_,
        commenttoa0_.ask_id as ask_id13_2_1_,
        commenttoa0_.estimate as estimat11_2_1_ 
    from
        comment_to_ask commenttoa0_ 
    where
        commenttoa0_.ask_id=?
Hibernate: 
    delete 
    from
        ask_image 
    where
        id=?
Hibernate: 
    delete 
    from
        ask_image 
    where
        id=?
Hibernate: 
    delete 
    from
        comment_to_ask 
    where
        comment_to_ask_id=?
Hibernate: 
    delete 
    from
        comment_to_ask 
    where
        comment_to_ask_id=?
Hibernate: 
    delete 
    from
        comment_to_ask 
    where
        comment_to_ask_id=?
Hibernate: 
    delete 
    from
        garage_ask 
    where
        id=?
Hibernate: 
    delete 
    from
        ask 
    where
        ask_id=?
Hibernate: 
    delete 
    from
        ask_image 
    where
        ask_id=?

심지어 ask의 이미지, 코멘트가 존재하는지를 확인하기 위한 select 쿼리까지 발생한다.

해서 본인은 ask와 연관관계가 있는 테이블들을 먼저 삭제한 뒤 ask를 마지막으로 삭제하면 쿼리가 훨씬 줄지 않을까 생각했다.

즉 서비스 레이어의 코드를 아래와 같이 수정하였다.

@Transactional
public void delete(Long askId, Long userId)
{
 	// .. 중략
    repository.deleteAllAskImageByAskId(askId);
    repository.deleteAllCommentByAskId(askId);
    repository.deleteById(askId);
	// ... 
}

위와 같이 했을 때, 물론 각각의 연관관계 객체들 삭제 쿼리는 하나씩만 생성되었으나 현재 cascade = ALL로 인해 Ask의 AskImage, Comment들을 select하는 쿼리는 여전히 발생했다.

Jpa는 현재 Ask와 AskImage, Comment들은 연관관계가 맺어져 있고, 부모 객체를 영속화, 삭제할때 자식 객체들도 같이 영속화, 삭제 되도록 해놨으므로 (cascade = ALL 옵션), AskImage, Comment가 존재하는지 조회를 한 뒤(이때 이들에 대한 select 쿼리가 발생) 있다면 삭제를 해준다.

하여 casecade를 save하는 과정에서만(persist) 설정하도록 Ask의 코드를 수정하였다.

@Entity
public class Ask {

    @OneToMany(mappedBy = "ask", cascade = PERSIST)
    private List<AskImage> askImages = new ArrayList<>();

    @OneToMany(mappedBy = "ask", cascade = PERSIST)
    private List<Comment> comments = new ArrayList<>();
}

이렇게 하고 다시 실행해보았다.

하지만 그 결과는 다음과 같다.

could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException:

현재 Ask의 id를 AskImage와 Comment 가 foreign key로 가지고 있어서, foreign key를 primary key로 가지는 Ask를 삭제하는 것은 안된다는 것이다. 왜냐하면 AskImage, Comment는 존재하지도 않는 Ask를 참조하는 것이 되기 때문이다. (고아 객체가 된다.)

 

하여 결국 Hard Delete가 아닌 Soft Delete를 하기로 했다. 현재 모든 테이블들에 is_enable 값을 저장하는데, is_enable값을 false로만 업데이트 해주고 조회시에는 이들을 제외하는 것이다.

Soft Delete를 하는 것이 삭제된 데이터를 복구하기도 용이하기 때문이다.

 

하여 Service 레이어의 코드를 아래와 같이 수정했다.

@Transactional
public void delete(Long askId, Long userId)
{
    // ... 중략
    ask.makeDisable();
    repository.makeDisableAllAskImageByAskId(askId);
    repository.makeDisableAllCommentImageByAskId(askId);
    // ... 중략
}

이와 같이 하면 3개의 update문만을 사용하여 delete를 구현할 수 있다.