• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

cascade=ALL, orphanRemoval=true 관련 질문

22.10.02 22:19 작성 조회수 428

2

안녕하세요.

혼자 고민하고 생각 해보다 해결이 되지 않아 문의드리러 왔습니다 ㅠㅠ

해당 수업을 듣고, 사이드 프로젝트를 수행하면서 orphanRemoval을 사용해보게 되었습니다.

대략적인 엔티티간의 관계는 다음과 같습니다.

총 4개의 엔티티가 각각 다대일 매핑으로 이루어져 있는데요.

각 엔티티의 정보는 다음과 같습니다.

  • 엔티티 A : 유저

  • 엔티티 B : 글

  • 엔티티 C : 글에 포함되는 정보

  • 엔티티 D : 엔티티 C에 포함되는 정보

문제는 엔티티 B까지만 cascade=ALL, orphanRemoval=true가 적용된다는 것입니다..ㅠㅠ

차이가 있다면, 엔티티 A는 유저의 정보라서, 항상 저장이 되어있는 것을 꺼내어와서 사용한다는 것이고(저장을 회원가입 로직에서만 수행합니다.),
엔티티 B, C, D는 글 작성 시 모두 새로 생성하여 사용한다는 점입니다.

저는 엔티티B 삭제 로직에서,
엔티티A의 List에서 엔티티B를 remove 하면 B, C, D가 모두 삭제되는 것을 기대했는데, 전혀 삭제가 되지 않습니다 ㅠ (List에서 엔티티B가 제거는 됩니다..)

오히려 엔티티A의 List에서 엔티티B을 remove하고, 추가적으로 엔티티B의 리포지토리에서 엔티티 B를 삭제해야만 비로소 삭제가 완료됩니다.

기능 자체를 구현은 했지만, 이렇게 동작하는 원인을 알고싶어서, 여기저기 찾아보다가 답을 찾지 못해 이곳에 질문 글을 남기게 되었습니다 ㅠㅜ

관련 코드는 아래 깃허브 리포지토리에 있습니다.

엔티티 리포지토리

https://github.com/SeolYoungKim/siders/tree/oauth2login/src/main/java/com/example/siderswebapp/domain

삭제 로직 리포지토리

https://github.com/SeolYoungKim/siders/tree/oauth2login/src/main/java/com/example/siderswebapp/service/post

답변 2

·

답변을 작성해보세요.

2

안녕하세요. 딱구님

제가 다음과 같이 테스트 코드를 만들어 돌려보았을 때는 모두 정상 동작하는 것을 확인할 수 있었습니다.

다음 코드를 참고해주세요.

package com.example.siderswebapp;

import com.example.siderswebapp.domain.Ability;
import com.example.siderswebapp.domain.fields.Fields;
import com.example.siderswebapp.domain.member.Member;
import com.example.siderswebapp.domain.member.RoleType;
import com.example.siderswebapp.domain.post.Post;
import com.example.siderswebapp.domain.tech_stack.TechStack;
import com.example.siderswebapp.service.post.PostService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import java.util.List;

@Transactional
@SpringBootTest
class SidersWebappApplicationTests {

    @Autowired
    PostService postService;

    @Autowired
    EntityManager em;

    @Test
    void jpaOnlyTest() {
        Member member = initData();

        Member findMember = em.find(Member.class, member.getId());
        findMember.getPostList().remove(0);
        em.flush();
    }

    @Test
    void serviceTest() throws IllegalAccessException {
        Member member = initData();
        Long postId = member.getPostList().get(0).getId();
        postService.deletePost2(postId, member.getAuthId());
        em.flush();
    }

    private Member initData() {
        Member member = Member.builder()
                .email("email")
                .authId("id")
                .picture("pic")
                .name("name")
                .roleType(RoleType.ADMIN)
                .build();

        Post post = Post.builder()
                .member(member)
                .title("title")
                .build();

        Fields fields = Fields.builder()
                .fieldsName("name")
                .recruitCount(1)
                .totalAbility(Ability.HIGH)
                .post(post).build();

        TechStack techStack = TechStack.builder()
                .stackName("stack")
                .fields(fields)
                .build();

        em.persist(member);
        em.persist(post);
        em.persist(fields);
        em.persist(techStack);

        em.flush();
        em.clear();
        return member;
    }

}

 

    public void deletePost2(Long id, String authId) throws IllegalAccessException {

        Post post = postRepository.findById(id)
                .orElseThrow(PostNotExistException::new);

        Member member = memberRepository.findByAuthId(authId)
                .orElseThrow(IllegalAccessException::new);

        member.getPostList().remove(post);
//        postRepository.delete(post); 코드 제거
    }

 

실행 로그

Hibernate: 
    delete 
    from
        tech_stack 
    where
        tech_stack_id=?
Hibernate: 
    delete 
    from
        fields 
    where
        fields_id=?
Hibernate: 
    delete 
    from
        post 
    where
        post_id=?
딱구님의 프로필

딱구

질문자

2022.10.03

ㅠㅠ 직접 해봐주셔서 너무 감사드립니다.

제가 테스트 코드에 갇혀서, 포스트맨으로 2차 검증을 해 볼생각을 못했네요..

선생님께서 문제가 없이 작동한다는 것을 보고, 제 테스트 코드가 잘못 되었음을 깨닫고, 포스트맨으로는 삭제가 되는지 1차적으로 먼저 확인을 해봤습니다. 그랬더니 삭제 기능이 잘 작동 되네요..

한동안 디버깅도 해보고 이래저래 테스트코드도 바꿔보고 하다가 고민 끝에 질문을 드렸었던 것인데, 테스트 코드가 잘못되었을 것이라고는 정말 상상도 못했습니다 ㅠㅠ 너무 부끄럽네요..

이제는 선생님께서 작성해주신 테스트코드 기준으로 재작성을 해봐야겠습니다.

이번 경험을 토대로 테스트 코드에 오류가 있을 수도 있음을 항상 생각해야겠습니다..

바쁘신 와중에(심지어 휴일인데..ㅠㅠ) 시간내어 피드백 주셔서 정말 감사드립니다.

 

-- 해결 했습니다! --

Test 코드에서 선생님처럼 memberRepository.save()의 위치를 엔티티들의 아래쪽으로 내렸더니 해결이 되었습니다.

아직은 Member를 저장 하고, Post에 할당하는 것과 모든걸 할당하고 마지막에 한번에 저장하는 것의 차이를 잘 모르겠습니다.

이러한 현상이 발생하는 이유를 몰랐던 것은, 아무래도 제가 JpaRepository의 동작 원리와 영속성 컨텍스트 등 JPA 관련 개념을 충분히 몰랐기 때문이라고 생각이 듭니다. 이 기회에 한번 더 복습하면서 이러한 현상이 발생했던 이유에 대해 깨달아 보도록 해야겠습니다.

다시한번 시간내어 확인해주셔서 감사드립니다.

1

딱구님의 프로필

딱구

질문자

2022.10.02

하나 의심이 되는건, 생성 시점이 다르기 때문일 것 같은데, 혹시 해당 이유일까요..?
영한쌤이 말씀하신 aggregate root 를 찾아보니 생성 시점에 따라 나눈다고 설명이 되어있어서, 그부분을 의심하고 있습니다.
저는 뭔가 추상적인 개념일 뿐이라고 생각했는데, JPA에서도 실제로 aggregate root 개념이 적용되어서, 각자 생성 시점이 다르면 해당 방법으로 관리가 되지 않는건가요??

몇 가지 테스트를 좀 더 해봤는데요.

  • 엔티티A를 "삭제" 할 경우에는 엔티티B, C, D가 모두 삭제됩니다.

    • 하지만 여전히 엔티티 A의 List에서 엔티티 B를 "remove"할 경우에는 삭제가 되지 않습니다.

     

이러한 결과를 미루어보았을 때, cascadeType=ALL만 제대로 작동한다고 봐도 될까요?

그렇다면 orphanRemoval은 왜 작동하지 않는걸까요..!