[TruobleShooting] 영속성 전이가 꼭 필요한 상황인가?

jckim22·2024년 2월 2일
0
post-thumbnail

Hanbat_Market에서 Reposiotry를 개발하던중 의문이 생겼다.

Item 일부분

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private Member member;

Article 일부분

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private Member member;

Member 일부분

@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<Item> items = new ArrayList<>();

위처럼 Member가 Item과 Article의 부모 엔티티로 지정이 되어있고 Item과 Article은 CascadeType.ALL로 영속성 전이 설정이 되어있다.

ItemRepository 아이템_멤버_연관성테스트

    @Test
    public void 아이템_멤버_연관성테스트() throws Exception {
        //given
        Member member = createTestMember();
        jpaMemberRepository.save(member);

        Item item = createTestItem(member);
        jpaItemRepository.save(item);

        //when
        Item findItem = jpaItemRepository.findById(item.getId()).get();
        Member findMember = jpaMemberRepository.findById(findItem.getMember().getId()).get();

        //then
        assertEquals(findItem, jpaItemRepository.findById(item.getId()).get());
        assertEquals(findMember, jpaMemberRepository.findById(member.getId()).get());

        assertEquals(item, findItem);
        assertEquals(member, findMember);
    }

근데 ItemRepository 테스트 중 위처럼 member를 먼저 persist 해주지 않으면 item에서 member를 조회하지 못해 예외가 나는 트러블이 발생했다.

계속 고민했다.

둘이 연관관계가 설정되어 있기 때문에 Item을 저장할 때 member가 저장이 되어야하는것 아닌가?

그러던 중 공부했던 내용이 떠올라서 아차했다.
"영속성 전이는 부모를 저장할 때 자식이 같이 저장되는 것"

너무 당연하고 쉬운 얘기였다.

member가 부모였고 item이 자식이었기 때문에 설정을 해놓고 member를 저장하는 순간 자동으로 item이 저장되는 것이었다.

그래서 코드를 아래처럼 변경했다.

    @Test
    public void 아이템_멤버_연관성테스트() throws Exception {
        //given
        Member member = createTestMember();

        Item item = createTestItem(member);

        jpaMemberRepository.save(member);

        //when
        Item findItem = jpaItemRepository.findById(item.getId()).get();
        Member findMember = jpaMemberRepository.findById(findItem.getMember().getId()).get();

        //then
        assertEquals(findItem, jpaItemRepository.findById(item.getId()).get());
        assertEquals(findMember, jpaMemberRepository.findById(member.getId()).get());

        assertEquals(item, findItem);
        assertEquals(member, findMember);
    }

ItemRepository 아이템_전체조회

    @Test
    public void 아이템_전체조회() throws Exception {
        //given
        Member member = createTestMember();

        Item item = createTestItem(member);
        Item item2 = createTestItem(member);
        Item item3 = createTestItem(member);

        jpaMemberRepository.save(member);

        Member findMember = jpaMemberRepository.findById(member.getId()).get();

        //when
        List<Item> items = jpaItemRepository.findAll();
        List<Item> memberItems = jpaItemRepository.findAllByMember(findMember);

        //then
        assertEquals(3, items.size());
        assertEquals(findMember.getItems().size(), items.size());
        assertEquals(findMember.getItems().size(), memberItems.size());
    }

위 테스트도 마찬가지였다.

더 나아가 ArticleRepositoryTest도 이런 문제들을 수정해주었다.

ArticleRepositoryTest

   @Test
    public void 게시글_아이템_멤버_연관성테스트() throws Exception {
        //given
        Member member = createTestMember();
        Item item = createTestItem(member, "PS5");
        Article article = creteTestArticle(member, item);

        jpaMemberRepository.save(member);

        //when
        Article findArticle = jpaArticleRepository.findById(article.getId()).get();
        Item findItem = jpaItemRepository.findById(findArticle.getItem().getId()).get();
        Member findMember = jpaMemberRepository.findById(findArticle.getMember().getId()).get();

        //then
        assertEquals(article, jpaArticleRepository.findById(article.getId()).get());
        assertEquals(findItem, jpaItemRepository.findById(item.getId()).get());
        assertEquals(findMember, jpaMemberRepository.findById(member.getId()).get());

        assertEquals(article, findArticle);
        assertEquals(item, findItem);
        assertEquals(member, findMember);
    }

    /**
     * 모든 게시글을 조회하는 것을 테스트 합니다.
     * 멤버와 아이템과 게시글의 연관성을 테스트합니다.
     */

    @Test
    public void 게시글_전체조회() throws Exception {
        //given
        Member member = createTestMember();
        Member member2 = createTestMember2();

        Item item = createTestItem(member, "PS3");
        Item item2 = createTestItem(member, "PS4");
        Item item3 = createTestItem(member2, "PS5");

        Article article = creteTestArticle(member, item);
        Article article2 = creteTestArticle(member, item2);
        Article article3 = creteTestArticle(member2, item3);

        jpaMemberRepository.save(member);
        jpaMemberRepository.save(member2);

        Member findMember = jpaMemberRepository.findById(member.getId()).get();

        //when
        List<Article> articles = jpaArticleRepository.findAll();
        List<Article> memberArticles = jpaArticleRepository.findAllByMember(findMember);

        //then
        assertEquals(3, articles.size());
        assertEquals(2, findMember.getItems().size());
        assertEquals(2, memberArticles.size());
    }

    @Test
    public void 게시글_검색() throws Exception {
        //given
        Member member = createTestMember();
        Member member2 = createTestMember2();

        Item item = createTestItem(member, "PS3");
        Item item2 = createTestItem(member, "PS4");
        Item item3 = createTestItem(member2, "PS5");

        Article article = creteTestArticle(member, item);
        Article article2 = creteTestArticle(member, item2);
        Article article3 = creteTestArticle(member2, item3);

        jpaMemberRepository.save(member);
        jpaMemberRepository.save(member2);

        ArticleSearchDto articleSearchDto = new ArticleSearchDto();

        //when
        List<Article> searchAll = jpaArticleRepository.findAllBySearch(articleSearchDto);

        articleSearchDto.setItemName("PS5");
        List<Article> searchByItemName = jpaArticleRepository.findAllBySearch(articleSearchDto);

        articleSearchDto.setItemName(null);
        articleSearchDto.setItemStatus(ItemStatus.COMP);
        item.changeItemStatus();

        List<Article> searchByItemStatus = jpaArticleRepository.findAllBySearch(articleSearchDto);

        //then
        assertEquals(searchAll.size(), 3);
        assertEquals(searchByItemName.size(), 1);
        assertEquals(searchByItemStatus.size(), 1);
    }

원래는 각 자식 엔티티마다 persist를 해주었지만 리팩토링 함으로써 훨씬 코드를 줄일 수 있었다.

하지만..(영속성 전이가 꼭 필요한 상황인가)

생각해보면 일반적으로 클라이언트가 Article과 Item은 동시에 저장하기 때문에 필요할 수도 있겠다.

하지만
Member는 원래 회원가입을 한 상태에서 게시글을 안올릴 수도 있고 올릴 수도 있는 것이다.
게시글과 아이템을 먼저 올리고 회원가입을 하지는 않는다.

결국 비즈니스 로직에서도 Member는 따로 persist를 하고 진행할 것이다.
그러므로 Member가 갖고 있던 CASCADE, 즉 영속성 전이 옵션은 빼는 것이 맞겠다고 판단했다.

Member

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 생성자를 통해서 값 변경 목적으로 접근하는 메시지들 차단
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "user_id")
    private Long id; //동시성 문제해결을 위해 추후에 AtomicLong 사용

    @Column(nullable = false, unique = true)
    private String mail;

    @Column(nullable = false)
    private String passwd;

    @Column(nullable = false, unique = true)
    private String phoneNumber;

    @Column(nullable = false, unique = true)
    private String nickname;

    @OneToMany(mappedBy = "member")
    private List<Article> articles = new ArrayList<>();

    @OneToMany(mappedBy = "member")
    private List<Item> items = new ArrayList<>();

    @OneToMany(mappedBy = "member")
    private List<PreemptionItem> preemptionItems = new ArrayList<>();

    @OneToMany(mappedBy = "member")
    private List<PurchaseHistory> purchaseHistories = new ArrayList<>();

    private Member(String mail, String passwd, String phoneNumber, String nickname) {
        this.mail = mail;
        this.passwd = passwd;
        this.phoneNumber = phoneNumber;
        this.nickname = nickname;
    }

    public static Member createMember(String mail, String passwd, String phoneNumber, String nickname) {
        return new Member(mail, passwd, phoneNumber, nickname);
    }
}

위처럼 회원가입을 따로 해서 따로 persist하는 Member에서 cascade를 타 빼주었다.

또한 생각해볼 것이 있다.

Item 등록이 먼저일까 Article 등록이 먼저일까?

생각해보면 Article을 등록하면서 자동으로 Item이 등록되는 것이 맞다.
근데 나의 원래 엔티티는 Item이 외래키를 갖고 연관관계의 주인이 되어있었다.

이 또한 아래처럼 수정했다.

Item

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "article_id")
    private Article article;

Article

    @OneToOne(mappedBy = "article", cascade = CascadeType.ALL)
    private Item item;

이제 테스트 코드를 수정해준다.

ArticleRepositoryTest

    @Test
    public void 게시글_등록() throws Exception {
        //given
        Member member = createTestMember();
        Item item = createTestItem(member, "PS5");
        Article article = creteTestArticle(member, item);

        //when
        jpaArticleRepository.save(article);

        //then
        assertEquals(article, jpaArticleRepository.findById(article.getId()).get());
    }

    @Test
    public void 게시글_아이템_멤버_연관성테스트() throws Exception {
        //given
        Member member = createTestMember();
        jpaMemberRepository.save(member);

        Item item = createTestItem(member, "PS5");
        Article article = creteTestArticle(member, item);

        jpaArticleRepository.save(article);

        //when
        Article findArticle = jpaArticleRepository.findById(article.getId()).get();
        Item findItem = jpaItemRepository.findById(findArticle.getItem().getId()).get();
        Member findMember = jpaMemberRepository.findById(findArticle.getMember().getId()).get();

        //then
        assertEquals(article, jpaArticleRepository.findById(article.getId()).get());
        assertEquals(findItem, jpaItemRepository.findById(item.getId()).get());
        assertEquals(findMember, jpaMemberRepository.findById(member.getId()).get());

        assertEquals(article, findArticle);
        assertEquals(item, findItem);
        assertEquals(member, findMember);
    }

    /**
     * 모든 게시글을 조회하는 것을 테스트 합니다.
     * 멤버와 아이템과 게시글의 연관성을 테스트합니다.
     */

    @Test
    public void 게시글_전체조회() throws Exception {
        //given
        Member member = createTestMember();
        Member member2 = createTestMember2();

        jpaMemberRepository.save(member);
        jpaMemberRepository.save(member2);

        Item item = createTestItem(member, "PS3");
        Item item2 = createTestItem(member, "PS4");
        Item item3 = createTestItem(member2, "PS5");

        Article article = creteTestArticle(member, item);
        Article article2 = creteTestArticle(member, item2);
        Article article3 = creteTestArticle(member2, item3);

        jpaArticleRepository.save(article);
        jpaArticleRepository.save(article2);
        jpaArticleRepository.save(article3);

        Member findMember = jpaMemberRepository.findById(member.getId()).get();

        //when
        List<Article> articles = jpaArticleRepository.findAll();
        List<Article> memberArticles = jpaArticleRepository.findAllByMember(findMember);

        //then
        assertEquals(3, articles.size());
        assertEquals(2, findMember.getItems().size());
        assertEquals(2, memberArticles.size());
    }

    @Test
    public void 게시글_검색() throws Exception {
        //given
        Member member = createTestMember();
        Member member2 = createTestMember2();

        jpaMemberRepository.save(member);
        jpaMemberRepository.save(member2);

        Item item = createTestItem(member, "PS3");
        Item item2 = createTestItem(member, "PS4");
        Item item3 = createTestItem(member2, "PS5");

        Article article = creteTestArticle(member, item);
        Article article2 = creteTestArticle(member, item2);
        Article article3 = creteTestArticle(member2, item3);

        jpaArticleRepository.save(article);
        jpaArticleRepository.save(article2);
        jpaArticleRepository.save(article3);

        ArticleSearchDto articleSearchDto = new ArticleSearchDto();

        //when
        List<Article> searchAll = jpaArticleRepository.findAllBySearch(articleSearchDto);

        articleSearchDto.setItemName("PS5");
        List<Article> searchByItemName = jpaArticleRepository.findAllBySearch(articleSearchDto);

        articleSearchDto.setItemName(null);
        articleSearchDto.setItemStatus(ItemStatus.COMP);
        item.changeItemStatus();

        List<Article> searchByItemStatus = jpaArticleRepository.findAllBySearch(articleSearchDto);

        //then
        assertEquals(searchAll.size(), 3);
        assertEquals(searchByItemName.size(), 1);
        assertEquals(searchByItemStatus.size(), 1);
    }

이제 member는 먼저 따로 persist하고 article에 item을 담아서 persist한다.

ItemRepositoryTest

    @Test
    public void 아이템_등록() throws Exception {
        //given
        Member member = createTestMember();

        Item item = createTestItem(member);
        //when
        jpaItemRepository.save(item);
        //then
        assertEquals(item, jpaItemRepository.findById(item.getId()).get());
    }

    @Test
    public void 아이템_멤버_연관성테스트() throws Exception {
        //given
        Member member = createTestMember();
        jpaMemberRepository.save(member);

        Item item = createTestItem(member);
        jpaItemRepository.save(item);

        //when
        Item findItem = jpaItemRepository.findById(item.getId()).get();
        Member findMember = jpaMemberRepository.findById(findItem.getMember().getId()).get();

        //then
        assertEquals(findItem, jpaItemRepository.findById(item.getId()).get());
        assertEquals(findMember, jpaMemberRepository.findById(member.getId()).get());

        assertEquals(item, findItem);
        assertEquals(member, findMember);
    }

    /**
     * 모든 아이템을 조회하는 것을 테스트 합니다.
     * 멤버와 이이템의 연관성을 테스트 합니다.
     */

    @Test
    public void 아이템_전체조회() throws Exception {
        //given
        Member member = createTestMember();
        jpaMemberRepository.save(member);

        Item item = createTestItem(member);
        Item item2 = createTestItem(member);
        Item item3 = createTestItem(member);
        jpaItemRepository.save(item);
        jpaItemRepository.save(item2);
        jpaItemRepository.save(item3);

        Member findMember = jpaMemberRepository.findById(member.getId()).get();

        //when
        List<Item> items = jpaItemRepository.findAll();
        List<Item> memberItems = jpaItemRepository.findAllByMember(findMember);

        //then
        assertEquals(3, items.size());
        assertEquals(findMember.getItems().size(), items.size());
        assertEquals(findMember.getItems().size(), memberItems.size());
    }

아이템레포지토리도 마찬가지로 member를 따로 회원가입 한다.

깔끔하게 테스트들이 통과된다.

정리

영속성 전이가 언제 필요한지 잘 고민해보자.

CASCADE 를 설정하면 연관관계에 있는 객체간의 작업이 서로에게 영향을 끼치게 된다. 그렇기 때문에 단일 엔티티에 완전히 종속적이거나 라이프사이클이 같은 객체들의 관계에 대해서만 사용해야한다.

사소하고 기본적인 것이었지만, 이러한 것들을 아무 생각없이 사용하지 말고 고민하면서 최대한의 효율을 낼 수 있도록 코드를 작성하자.

profile
개발/보안

0개의 댓글

관련 채용 정보