-> 블로그 이전

[JPA] 영속성 전이 & 고아 객체

2022. 7. 6. 15:27Language`/JPA

영속성 전이 : CASCADE

특정 엔티티를 "영속 상태"로 만들 때 → 연관된 엔티티도 함께 영속 상태로 만들고 싶다면 어떻게 해야할까

>> 영속성 전이 기능 (Transitive Persistence)

  • JPA에서는 영속성 전이를 CASCADE 옵션을 통해서 제공한다
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id", nullable = false)
    private Team team;

    public void joinTeam(Team team){
        if(this.team != null){
            this.team = null;
        }

        this.team = team;
        team.getMemberList().add(this);
    }
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "team")
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;

    @Column(name = "team_name")
    private String teamName;

    @OneToMany(mappedBy = "team")
    private List<Member> memberList = new ArrayList<>();
}

여기서 Team Entity를 persist하게 되면 이와 연관된 Member List들도 전부 persist하고싶어졌다


CascadeType.PERSIST

(1) 영속성 전이(CascadeType.PERSIST) 사용 X

// 팀 저장
Team team = new Team("teamA");
em.persist(team);

// memberA 저장
Member memberA = new Member("memberA");
memberA.joinTeam(team);            
em.persist(memberA);

// memberB 저장
Member memberB = new Member("memberB");
memberB.joinTeam(team);
em.persist(memberB);

현재 team, memberA, memberB 전부 em.persist를 통해서 영속화 시키고 team의 List<Member>에는 memberA, memberB가 존재할 것이다

 

JPA에서는 엔티티를 저장할 때 "연관된 모든 엔티티는 영속 상태"이여야 한다

따라서 최종적으로 Team Entity를 저장하려면 연관된 모든 Member Entity가 영속상태이여야 한다

 

"만약 Member Entity가 영속상태가 아니라면 Team Entity를 저장할 때 어떤 일이 발생할까"

// team 저장 (영속화)
Team team = new Team("teamA");
em.persist(team);

// memberA (비영속)
Member memberA = new Member("memberA");
memberA.joinTeam(team);

// memberB (비영속)
Member memberB = new Member("memberB");
memberB.joinTeam(team);

당연히 Member는 저장되지 않고 team으로부터 List<Member>를 찾아도 Member가 없는 것을 확인할 수 있다

 

>> 여기서 "영속성 전이"를 사용하면 Team만 영속 상태로 만들면 그와 연관된 List<Member>까지 한번에 영속 상태로 만들 수 있다


2. 영속성 전이(CascadeType.PERSIST) 사용 O

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "team")
public class Team {
    ...
    ...

    @OneToMany(mappedBy = "team", cascade = CascadeType.PERSIST)
    private List<Member> memberList = new ArrayList<>();
}

Team의 List<Member>에 "cascade = CascadeType.PERSIST"를 설정해주면 Team과 연관된 "List<Member>에 존재하는 Member"는 Team을 persist하면 자동으로 전파되어서 persist된다

// team 저장 (영속)
Team team = new Team("teamA");
em.persist(team);

// memberA (비영속)
Member memberA = new Member("memberA");
memberA.joinTeam(team);

// memberB (비영속)
Member memberB = new Member("memberB");
memberB.joinTeam(team);

em.flush();

System.out.println("memberA 영속화? = " + em.contains(memberA));
System.out.println("memberB 영속화? = " + em.contains(memberB));

위에서 member가 저장되지 않았던 코드 그대로이다. 바뀐 것은 Team의 List<Member>에 CascadeType.PERSIST를 지정해준 것 뿐이다

 

그리고 em.flush()를 통해서 쿼리를 DB로 날린 후, memberA, memberB가 영속성 컨텍스트에서 영속화 되었는지 확인을 해보자

  • em.flush()를 한다고 해서 영속성 컨텍스트가 clear되는 것은 "절대 아니다". 단지 SQL 저장소의 모든 쿼리가 DB로 날라갈 뿐이다
Hibernate: 
    /* insert cascade.domain.Team
        */ insert 
        into
            team
            (team_name) 
        values
            (?)
            
Hibernate: 
    /* insert cascade.domain.Member
        */ insert 
        into
            member
            (name, team_id) 
        values
            (?, ?)
            
Hibernate: 
    /* insert cascade.domain.Member
        */ insert 
        into
            member
            (name, team_id) 
        values
            (?, ?)
            
memberA 영속화? = true
memberB 영속화? = true

보면 memberA, memberB를 따로 em.persist를 통해서 영속화하지 않았음에도 불구하고 영속화가 되고, 그에 따라서 DB로 insert query가 날라감을 확인할 수 있다

>> CascadeType.PERSIST로 인해서 Member들을 따로 persist하지 않아도 Team을 persist하면 그와 연관된 List<Member>까지 persist가 전파되어서 자동으로 List<Member>들도 persist됨을 확인할 수 있다

영속성 전이는 단지 "엔티티를 영속화할 때 연관된 엔티티도 자동으로 영속화"해주는 기능일뿐, 연관관계 매핑과는 전혀 관련이 없다

CascadeType.REMOVE

(1) 영속성 전이(CascadeType.REMOVE) 사용 X

CascadeType.REMOVE를 사용하지 않고 Team만 제거해버리면 어떤일이 발생할까

Team findTeam = em.find(Team.class, 1L);
em.remove(findTeam);

WARN: SQL Error: 1451, SQLState: 23000
ERROR: Cannot delete or update a parent row: a foreign key constraint fails (`jpa`.`member`, CONSTRAINT `member_ibfk_1` FOREIGN KEY (`team_id`) REFERENCES `team` (`team_id`))

Team의 FK를 보유하고 있는 Member를 지우기도 전에 Team을 remove시켜버리려고 하면 당연히 참조 무결성 제약조건을 위배하게 되어서 에러가 발생한다

 

따라서 team(pk = 1)을 제거하려면 먼저 해당 team에 속한 member부터 제거해준 다음에 team을 제거해야 한다

Team findTeam = em.find(Team.class, 1L);
Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 2L);

em.remove(findMember1);
em.remove(findMember2);
em.remove(findTeam);

(2) 영속성 전이(CascadeType.REMOVE) 사용 O

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "team")
public class Team {
    ...
    ...

    @OneToMany(mappedBy = "team", cascade = CascadeType.REMOVE)
    private List<Member> memberList = new ArrayList<>();
}

이렇게 CascadeType.REMOVE로 설정하게 되면 Team에 대한 em.remove를 하게되면 해당 Team과 연관된 List<Member>의 Member들이 자동적으로 같이 em.remove된다

 

Team findTeam = em.find(Team.class, 1L);
em.remove(findTeam);
Hibernate: 
    /* delete cascade.domain.Member */ delete 
        from
            member 
        where
            member_id=?
            
Hibernate: 
    /* delete cascade.domain.Member */ delete 
        from
            member 
        where
            member_id=?
            
Hibernate: 
    /* delete cascade.domain.Team */ delete 
        from
            team 
        where
            team_id=?

영속성 전이(삭제) 설정을 해주니까 em.remove()만 해줘도 먼저 연관된 List<Member>들의 delete query가 나가게 되고 그 다음에 team에 대한 delete query가 나감을 확인할 수 있다

  • FK 제약조건을 고려해서 "연관된 Member"가 먼저 삭제되고 그 다음에 Team이 삭제되는 것이다

CASCADE 종류

cascade는 여러 속성을 한번에 사용할 수 있다

@OneToMany(mappedBy = "team", cascade = {CascadeType.REMOVE, CascadeType.PERSIST})
private List<Member> memberList = new ArrayList<>();
  • team을 persist하면 Member는 자동적으로 persist되고, team을 remove하면 Member도 자동적으로 remove된다
PERSIST & REMOVE는 실행할 때 바로 전이가 발생되는게 아니라 "Flush를 호출할 때" 전이가 발생한다

 


고아 객체 (orphanRemoval)

JPA에서는 부모 엔티티와 관계가 끊어진 자식 엔티티를 "자동으로 삭제"하는 기능을 제공해준다

Cascade.REMOVE → 부모 엔티티가 삭제되면 자동적으로 연관된 자식 엔티티들도 삭제
orphanRemoval = true → 부모 엔티티와 "관계가 끊어진" 자식 엔티티를 자동 삭제
@OneToMany(mappedBy = "team", orphanRemoval = true)
private List<Member> memberList = new ArrayList<>();

현재 Team의 List<Member>에는 {memberA, memberB, memberC}가 있다고 하자

여기서 "orphanRemoval = true"로 설정하고나서, memberC를 List에서 제거해버리면 memberC는 단순히 List에서만 제거되는게 아니라 아예 데이터 자체가 제거된다

  • 그리고 orphanRemoval = true로 설정하고 부모 엔티티를 제거해버리면 개념적으로 볼 때 자식은 고아가 되기 때문에 자식도 같이 제거된다 -> CascadeType.REMOVE로 설정한 것과 같다

 

※ orphanRemoval = true 관련 오류

 

[HHH-9330] - Hibernate JIRA

회사에서 관리하는 프로젝트에 참여하고 있습니다

hibernate.atlassian.net

 

 

orphanRemoval 테스트 문제 · Issue #1 · jyami-kim/Jyami-Java-Lab

https://jyami.tistory.com/22 - 혼자 정리한 자료 : 제일 아랫단 링크 주소 [Parent.java] https://github.com/mjung1798/spring-boot/blob/master/jpa-lab/src/main/java/com/jyami/jpalab/domain/Parent.java [Child.java] htt...

github.com

현재 orphanRemoval = true만 설정해놓고 돌리게 되면 자식 엔티티들이 삭제되지 않는 오류가 있다고 한다.

이 오류는 Hibernate측에서 2015년 쯤에 발표한 issue이고, <Hibernate 4.3.8.Final>에서는 정상동작 되었지만 추후 다른 이슈로 인해 Rollback이 되어서 다시 오류가 발생하였다고 한다

  • 다른 이슈 = OneToOne 관계에서 cascade 없이 자식 엔티티를 flush할 경우 error가 발생해야 하는데 orphanRemoval = true로 지정하면 에러가 발생되지 않아서 Rollback

 

따라서 orphanRemoval = true를 사용하려면 CascadeType을 같이 사용해야 오류가 나지 않을 거라고 생각한다

 

<orphanRemoval = true만 사용>

@OneToMany(mappedBy = "team", orphanRemoval = true)
private List<Member> memberList = new ArrayList<>();
Team findTeam = em.find(Team.class, 1L);
findTeam.getMemberList().remove(0);
Hibernate: 
    select
        team0_.team_id as team_id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
Hibernate: 
    select
        memberlist0_.team_id as team_id3_0_0_,
        memberlist0_.member_id as member_i1_0_0_,
        memberlist0_.member_id as member_i1_0_1_,
        memberlist0_.name as name2_0_1_,
        memberlist0_.team_id as team_id3_0_1_ 
    from
        member memberlist0_ 
    where
        memberlist0_.team_id=?

remove를 했는데 delete query가 나가지 않는 것을 확인할 수 있다

 

<orphanRemoval = true + CascadeType.ALL>

@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Member> memberList = new ArrayList<>();
Team findTeam = em.find(Team.class, 1L);
findTeam.getMemberList().remove(0);
Hibernate: 
    select
        team0_.team_id as team_id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
Hibernate: 
    select
        memberlist0_.team_id as team_id3_0_0_,
        memberlist0_.member_id as member_i1_0_0_,
        memberlist0_.member_id as member_i1_0_1_,
        memberlist0_.name as name2_0_1_,
        memberlist0_.team_id as team_id3_0_1_ 
    from
        member memberlist0_ 
    where
        memberlist0_.team_id=?
        
Hibernate: 
    /* delete cascade.domain.Member */ delete 
        from
            member 
        where
            member_id=?

원하는대로 delete query가 나가는 것을 확인하였다

 

orphanRemoval = true만 썻을 경우 발생하는 오류는 Hibernate측의 오류이고 JPA의 구현체중 하나인 EclipseLink를 사용하면 정상적으로 동작된다고 알려져 있습니다