JPA

프록시

No-ah98 2022. 4. 7. 17:50

목차


프록시

  • 프록시에 대해 설명하기 전에, 이 기술이 나오게 된 배경을 알아보자
  • Member와 Team이 연관 관계 되어있다고 했을 때, Member를 조회할 때 Team도 함께 조회 해야할까?
    • 비즈니스 로직에 따라서는 Member만 조회하는 경우가 있을 것이다. 즉, Team까지 조회 할 필요가 없을 때 다 같이 조회하는거는 낭비다.
    • 이를 지연로딩과 프록시라는 개념으로 해결할 수 있다.

 

프록시 기초

  • 지연 로딩을 이해하기 위해서는 프록시 개념에 대한 이해가 필요하다.
  • JPA에서는 em.find() 와 em.getReference() 메소드를 제공한다.
    • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
      • DB 쿼리는 안나가는데, 객체가 조회됨

  • em.find() 멤버 조회 -> DB에 쿼리가 바로 발생
Member member = new Member();
member.setCreatedBy("creator");

em.persist(member);

em.flush();
em.clear();

Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());

tx.commit();
Hibernate: 
    /* insert hello.jpa.Member
        */ insert 
        into
            Member
            (id, createdBy, createdDate, lastModifiedBy, lastModifiedDate, age, description, locker_id, roleType, name) 
        values
            (null, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        member0_.id as id1_4_0_,
        member0_.createdBy as createdB2_4_0_,
        member0_.createdDate as createdD3_4_0_,
        member0_.lastModifiedBy as lastModi4_4_0_,
        member0_.lastModifiedDate as lastModi5_4_0_,
        member0_.age as age6_4_0_,
        member0_.description as descript7_4_0_,
        member0_.locker_id as locker_10_4_0_,
        member0_.roleType as roleType8_4_0_,
        member0_.team_id as team_id11_4_0_,
        member0_.name as name9_4_0_,
        locker1_.id as id1_3_1_,
        locker1_.name as name2_3_1_,
        team2_.id as id1_8_2_,
        team2_.createdBy as createdB2_8_2_,
        team2_.createdDate as createdD3_8_2_,
        team2_.lastModifiedBy as lastModi4_8_2_,
        team2_.lastModifiedDate as lastModi5_8_2_,
        team2_.name as name6_8_2_ 
    from
        Member member0_ 
    left outer join
        Locker locker1_ 
            on member0_.locker_id=locker1_.id 
    left outer join
        Team team2_ 
            on member0_.team_id=team2_.id 
    where
        member0_.id=?
findMember.id = 1
findMember.username = creator
  • em.getReference() 멤버 조회 -> 실제로 필요한 시점에 데이터베이스에 쿼리가 나간다.
    • findMember.username 필드 출력할 때, DB에서 조회가 필요하므로 쿼리가 발생
    • findMember.getClass()를 통해 객체를 확인해보면 Member객체가 아닌, 하이버 네이트가 강제로 만든 가짜 클래스 HibernateProxy 객체를 확인할 수 있다.
Member member = new Member();
member.setUsername("creator");

em.persist(member);

em.flush();
em.clear();

Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());

tx.commit();
Hibernate: 
    /* insert hello.jpa.Member
        */ insert 
        into
            Member
            (id, createdBy, createdDate, lastModifiedBy, lastModifiedDate, age, description, locker_id, roleType, name) 
        values
            (null, ?, ?, ?, ?, ?, ?, ?, ?, ?)
findMember = class hello.jpa.Member$HibernateProxy$yJgMgbkR
findMember.id = 1
Hibernate: 
    select
        member0_.id as id1_4_0_,
        member0_.createdBy as createdB2_4_0_,
        member0_.createdDate as createdD3_4_0_,
        member0_.lastModifiedBy as lastModi4_4_0_,
        member0_.lastModifiedDate as lastModi5_4_0_,
        member0_.age as age6_4_0_,
        member0_.description as descript7_4_0_,
        member0_.locker_id as locker_10_4_0_,
        member0_.roleType as roleType8_4_0_,
        member0_.team_id as team_id11_4_0_,
        member0_.name as name9_4_0_,
        locker1_.id as id1_3_1_,
        locker1_.name as name2_3_1_,
        team2_.id as id1_8_2_,
        team2_.createdBy as createdB2_8_2_,
        team2_.createdDate as createdD3_8_2_,
        team2_.lastModifiedBy as lastModi4_8_2_,
        team2_.lastModifiedDate as lastModi5_8_2_,
        team2_.name as name6_8_2_ 
    from
        Member member0_ 
    left outer join
        Locker locker1_ 
            on member0_.locker_id=locker1_.id 
    left outer join
        Team team2_ 
            on member0_.team_id=team2_.id 
    where
        member0_.id=?
findMember.username = creator

프록시 특징

  • 실제 클래스를 상속 받아서 만들어짐
    • 하이버네이트가 내부적으로 상속받아서 만든다.
  • 실제 클래스와 겉 모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)

  • 프록시 객체는 실제 객체의 참조(target)을 보관
  • 프록시 객체를 호출하면 프록시 객체는 실체 객체의 메소드 호출

프록시 객체의 초기화

  1. em.getReference()로 가져온 프록시 객체에서 getName()을 호출하면
  2. MemberProxy 객체에 target이 초기화가 안됐으므로, 영속성 컨텍스트에 Member 객체를 요청한다.
  3. 영속성 컨텍스트는 DB에서 조회를 하여 실제 Entity를 생성한다.
  4. 그리고 target이 실제 Member를 연결 시키므로 target.getName()을 호출해서 member.getName()을 반환한다.
  5. 프록시 객체에 target이 할당이 되면, 프록시 객체의 초기화 동작은 더 이상 없어도 된다.

Member member = em.getReference(Member.class, member.getId());
member.getName();

프록시 특징 정리

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 인티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
    • 정확히는 프록시 객체의 target에 값이 실제 객체로 초기화 됨.
  • 프록시 객체는 원본 엔티티를 상속받음. 따라서 타입 체크시 주의해야한다 (== 비교 실패, 대신 instance of 사용)
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
    • 하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
Member find = em.find(Member.class, member1.getId());
Member reference = em.getReference(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1 == m2));
find == reference : false
System.out.println("find : " + (find instanceof Member));
System.out.println("reference : " + (reference instanceof Member));
find : true
reference : true
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환. (반대도 마찬가지)
    • 영속성 컨텍스트에 이미 객체가 존재한다면 굳이 프록시 객체를 통해 반환 할 필요가 없다( 굳이..?😨)
    • JPA는 하나의 영속성 컨텍스트에서 조회하는 같은 엔티티 동일성을 보장한다.
      • JPA가 기본적으로 제공하는 메커니즘 중 하나. 반복가능한 읽기(repeatable read) 제공
    • 따라서, 아래의 코드에서 두 객체는 같다. em.getReference() 메소드를 사용해도 프록시 객체를 반환하지 않는다.

 

Member find = em.find(Member.class, member.getId());
Member reference = em.getReference(Member.class, member.getId());

System.out.println("find == reference : " + (find == reference)); // true
  • 둘 다 getRefernect()로 가져오면?
    • 결론은 둘다 프록시 객체이다. 여기서 중요한 점은 "같은 프록시 객체"이다
      • JPA는 한 트랜잭션에서 조회하는 같은 엔티티의 동일성을 보장한다.
Member reference1 = em.getReference(Member.class, member.getId());
Member reference2 = em.getReference(Member.class, member.getId());

System.out.println("reference1 == reference2 : " + (reference1 == reference2)); // true
  • getRefernect()로 먼저 가져오고, find()로 실제 객체를 조회하면?
    • 결론은 둘 다 같은 프록시 객체를 반환한다.
      • 엥? find()는 실제 객체를 반환해야 하는거 아닌가?
      • 이는 "JPA는 한 트랜잭션에서 조회하는 같은 엔티티의 동일성을 보장한다" 때문에
      • 한 트랜잭션 내에서 reference == find를 true로 반환하기 위해 이렇게 동작한다.
      • 참고로 JPA에서 내부적으로 다 처리를 해주기 때문에, 개발할때는 프록시던 진짜 객체던 중요하지는 않다. 그냥 멤버 조회하면서 개발 하면 된다
Member reference = em.getReference(Member.class, member.getId());
Member find = em.find(Member.class, member.getId());

System.out.println("reference == find : " + (reference == find)); // true
  • 실무에서 많이 만나게 되는 문제
    • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 초기화 문제가 발생한다.
      • 트랜잭션 범위 밖에서 프록시 객체를 조회할 때
        • 하이버네이트는 org.hibernate.LazyInitializationException 예외가 발생
        • em.detach(), em.close(), em.clear() 모두 위와 같은 에러가 발생
        • 프록시 객체를 초기화 할 수 없기 때문에 더 이상 영속성 컨텍스트 도움을 받지 못한다.
Member member = new Member();
member.setUsername("creator");

em.persist(member);

em.flush();
em.clear();

Member reference = em.getReference(Member.class, member.getId());

em.detach(reference);
//em.close도 동일

System.out.println("findMember.username = " + reference.getUsername());

tx.commit();

즉시 로딩과 지연 로딩 

  • 위 개념을 이해하기 위해 프록시에 대한 개념을 선행했다.
  • 다시 처음 질문으로 돌아가 보자.
    • Member를 조회할 때 Team을 함께 조회 해야할까?
    • 비즈니스 로직에서 단순히 Member만 조회하는 거라면 Team이 연관관계 되어있더라도 같이 조회하는 것은 낭비이다.
  • JPA는 위 질문을 지연로딩 LAZY를 사용해서 프록시로 조회하는 방법으로 해결한다.

코드

  • Member와 Team이 다대일(@ManyToOne)로 매핑되어 있는 상황에서,
  • @ManyToOne 에노테이션에 fetch 타입을 줄 수 있다.
  • Fetch.LAZY

Member

@Entity
@Getter
@Setter
public class Member extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String username;

    private Integer age;

    @Enumerated(EnumType.STRING)
    private RoleType roleType;

    @Lob
    private String description;

    // 패치 타입 LAZY 설정
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id", insertable = false, updatable = false)
    private Team team;


    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts = new ArrayList<>();

    public void setTeam(Team team) {
        this.team = team;
        this.team.getMembers().add(this);
    }
}

Main( Member를  조회하고 Team 객체의 조회를 한 시점에 쿼리가 나간다. )

            Member member = new Member();
            member.setUsername("memberA");
            member.setCreatedBy("helloA");
            member.setCreatedDate(LocalDateTime.of(2019,11,12,12,12));
            em.persist(member);

            Team team = new Team();
            member.setTeam(team);
            em.persist(team);

            em.flush();
            em.clear();

            Member findMember = em.find(Member.class, member.getId());
            System.out.println("m = " + findMember.getTeam().getClass());

            System.out.println("=============================================");
            findMember.getTeam().getName(); // 이 시점에 쿼리가 나감 
            System.out.println("=============================================");
            transaction.commit();
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_3_0_,
        member0_.createdBy as createdb2_3_0_,
        member0_.createdDate as createdd3_3_0_,
        member0_.lastModeifiedDate as lastmode4_3_0_,
        member0_.lastModifiedBy as lastmodi5_3_0_,
        member0_.TEAM_ID as team_id7_3_0_,
        member0_.name as name6_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
m = class hellojpa.Team$HibernateProxy$dfWJNi0B
=============================================
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_7_0_,
        team0_.name as name2_7_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
=============================================

지연 로딩(LAZY)

  • 연관 관계 어노테이션에 지연 로딩(LAZY)을 설정하면 아래 이미지와 같이 동작한다.
  • 로딩되는 시점에 Lazy로 설정되어 있는 Team 엔티티는 프록시 객체로 가져온다.
  • 로직에서 실제 객체를 사용할 때(Team) 초기화가 되고 DB에 쿼리가 나간다.
    • getTeam()으로 Team을 조회하면 프록시 객체가 조회가 된다.
    • getTeam().xxx()으로 Team 객체의 필드에 접근할 때 쿼리가 나간다.

즉시 로딩(EAGER)

  • 즉시 로딩은 쉽게 말하면 연관된 객체를 한방 쿼리로 다 조회 하는 것이다.
  • fetch 타입을 EAGER로 설정하면 즉시 로딩으로 설정된다.
  • 실행 결과를 보면 프록시 객체가 아닌 실제 객체가 조회된다.
@Entity
@Getter
@Setter
public class Member extends BaseEntity {

    ...

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "team_id", insertable = false, updatable = false)
    private Team team;

    ...
}
Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member = new Member();
member.setUsername("memberA");
em.persist(member);

member.setTeam(team);

em.flush();
em.clear();

Member findMember = em.find(Member.class, member.getId());

System.out.println(findMember.getTeam().getClass());
System.out.println("TEAM NAME : " + findMember.getTeam().getName());
tx.commit();
실행 결과

Hibernate: 
    select
        member0_.id as id1_4_0_,
        member0_.createdBy as createdB2_4_0_,
        member0_.createdDate as createdD3_4_0_,
        member0_.lastModifiedBy as lastModi4_4_0_,
        member0_.lastModifiedDate as lastModi5_4_0_,
        member0_.age as age6_4_0_,
        member0_.description as descript7_4_0_,
        member0_.locker_id as locker_10_4_0_,
        member0_.roleType as roleType8_4_0_,
        member0_.team_id as team_id11_4_0_,
        member0_.name as name9_4_0_,
        locker1_.id as id1_3_1_,
        locker1_.name as name2_3_1_,
        team2_.id as id1_8_2_,
        team2_.createdBy as createdB2_8_2_,
        team2_.createdDate as createdD3_8_2_,
        team2_.lastModifiedBy as lastModi4_8_2_,
        team2_.lastModifiedDate as lastModi5_8_2_,
        team2_.name as name6_8_2_ 
    from
        Member member0_ 
    left outer join
        Locker locker1_ 
            on member0_.locker_id=locker1_.id 
    left outer join
        Team team2_ 
            on member0_.team_id=team2_.id 
    where
        member0_.id=?
        
class hello.jpa.Team

TEAM NAME : teamA

 

프록시와 즉시 로딩 주의할 점

  • 실무에서는 가급적 지연 로딩만 사용하자. (즉시 로딩은 사용하지 말자)
    • 그럼 여기서 의문점이 생긴다. 한방 쿼리로 모두 가져오면 필요할 때 마다 쓸 수 있는거 아닌가?
    • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
      • 만약 ManToOne 어노테이션이 5개가 있고 전부 EAGER로 설정됐다고 하자
      • Join이 5개가 일어난다.. 실무에선 테이블이 훨씬 많을 것이다
  • 즉시 로딩은 N+1 문제를 일으킨다.
    • 실무에서 복잡한 쿼리를 많이 풀어내기 위해 JPQL을 사용한다. 
    • em.find()는 PK를 정해놓고 DB에서 가져오기 때문에 JPA 내부에서 최적화를 할 수 있다. (한방쿼리)
    • 하지만, JPQL에선 입력 받은 query string이 그대로 SQL로 변환된다.
      • "Slect m from Member m"이 문장은 Member만 조회한다.
      • Member 객체를 보니깐
    • 엉? Member 엔티티의 Team이 EAGER로 즉시 로딩 됐네?
      • LAZY면 프록시를 넣으면 되지만, EAGER는 반환되는 시점에 이미 다 조회가 되어 있어야 한다.
      • 따라서, Member를 다 가져오고 연간된 Team을 다시 가져와야 되는 일이 발생한다.

코드로 이해( 멤버가 2명, 팀은 2개이며 모든 멤버를 조회하는 경우)

Team team1 = new Team();
team1.setName("teamA");
em.persist(team1);

Team team2 = new Team();
team2.setName("teamB");
em.persist(team2);

Member member1 = new Member();
member1.setUsername("memberA");
em.persist(member1);
member1.setTeam(team1);

Member member2 = new Member();
member2.setUsername("memberB");
em.persist(member2);
member2.setTeam(team2);

em.flush();
em.clear();

List<Member> members = em
                .createQuery("select m from Member m", Member.class)
  .getResultList();

tx.commit();
  • 실행 결과를 보면, 멤버를 가져오고,
  • 멤버에 EAGER로 설정되어 있으므로 비어있는 Team을 채우기 위해 TEAM을 각각 쿼리를 날려서 가져온다.
  • 만약 멤버가 100명이라고 생각해보자.
    • Team을 채우기 위해 쿼리가 100개 날라갈 것이다.. 그래서 실무에서는 사용하지 말라는 것이다. 
  • 이제 N+1의 의미를 이해할 수 있다.
    • 1은 Member를 조회하기 위해 날리는 쿼리이고, N은 예상치 못한 추가 쿼리가 N개가 발생한다는 것이다.
Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.id as id1_4_,
            member0_.createdBy as createdB2_4_,
            member0_.createdDate as createdD3_4_,
            member0_.lastModifiedBy as lastModi4_4_,
            member0_.lastModifiedDate as lastModi5_4_,
            member0_.age as age6_4_,
            member0_.description as descript7_4_,
            member0_.locker_id as locker_10_4_,
            member0_.roleType as roleType8_4_,
            member0_.team_id as team_id11_4_,
            member0_.name as name9_4_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.id as id1_8_0_,
        team0_.createdBy as createdB2_8_0_,
        team0_.createdDate as createdD3_8_0_,
        team0_.lastModifiedBy as lastModi4_8_0_,
        team0_.lastModifiedDate as lastModi5_8_0_,
        team0_.name as name6_8_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
Hibernate: 
    select
        team0_.id as id1_8_0_,
        team0_.createdBy as createdB2_8_0_,
        team0_.createdDate as createdD3_8_0_,
        team0_.lastModifiedBy as lastModi4_8_0_,
        team0_.lastModifiedDate as lastModi5_8_0_,
        team0_.name as name6_8_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
  • 결론은 즉시 로딩보다는 가급적 그냥 지연 로딩 전략을 사용하자.
    • "그러면 실무에서 모두 조회하고 싶은 경우가 대부분이여도 LAZY로 설정해야 하나?"
    • 이를 해겨랗고자 JPQL의 fetch join을 통해서 해당 시점에 한방 쿼리로 가져와서 쓸 수 있다.
      • 추가적으로 엔티티그래프, 어노테이션으로 푸는 방법, 배치 사이즈 설정으로도 해결할 수는 있다.
      • 대부분 fetch join으로 해결한다.
  • @ManyToOne, @OneToOne과 같이 @xxToOne은 기본적으로 EAGER 전략을 사용한다.
    • 전부 LAZY로 설정해주자 

지연 로딩 활용

  • Member와 Team을 자주 함께 사용한다 -> 즉시 로딩
  • Member와 Order는 가끔 사용한다 -> 지연 로딩
  • Order와 Product는 자주 함꼐 사용한다 -> 즉시 로딩
  • 위와 같이 설정해 놓고 쓸 수 있지만, 굉장히 이론적인 개념이고
  • 실무에서는 다 LAZY로 쓰자. 즉시 로딩 사용하지 말자.
  • JPQL fetch join이나, 엔티티 그래프 기능으로 해결하자.
  • 즉시 로딩은 상상하지 못한 쿼리가 나간다.

영속성 전이 : CASCADE

  • 영속성 전이는 즉시, 지연 로딩이나 연관관계 설정과 전혀 관계가 없다. ( 흔히들 착각한다!! )
  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다.
  • 예로 들면, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하고 싶은 경우

코드로 이해( 다대다 관계인 Parient, Child를 양방향 연관관계 매핑 )

Child

@Entity
@Getter
@Setter
public class Child {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

Parent( 연관 관계 편의 메소드 추가 )

@Entity
@Getter
@Setter
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent")
    private List<Child> children = new ArrayList<>();

    public void addChild(Child child) {
        this.children.add(child);
        child.setParent(this);
    }
}
  • 원래 였으면 Main에서 저장할 때, em.persist를 3번 해야한다.
    • 그런데, 개발 하다가 Parent가 Child를 관리해줬으면 한다.
    • 이런 상황에서 CASCADE를 활용하면 된다.
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);
em.persist(child1);
em.persist(child2);

tx.commit();
  • Parent의 @OneToMany에서 CASCADE 옵션 추가 
@Entity
@Getter
@Setter
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) //cascade 추가
    private List<Child> children = new ArrayList<>();

    public void addChild(Child child) {
        this.children.add(child);
        child.setParent(this);
    }
}
  • 이전 상황과 달리 Parent만 em.persist
    • 정상적으로 insert 쿼리 3개가 나간다 
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);

tx.commit();
Hibernate: 
    /* insert cascade.Parent
        */ insert 
        into
            Parent
            (id, name) 
        values
            (null, ?)
Hibernate: 
    /* insert cascade.Child
        */ insert 
        into
            Child
            (id, name, parent_id) 
        values
            (null, ?, ?)
Hibernate: 
    /* insert cascade.Child
        */ insert 
        into
            Child
            (id, name, parent_id) 
        values
            (null, ?, ?)

CACADE 주의사항

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.
  • @OneToMany(mappedBy = ) 선언된 쪽에 다 걸어야 되나?
    • 아니다.
    • 단독 부모가 자식들을 관리할 때 의미가 있다.
    • 게시판을 예로 들면, 하나의 게시글(부모)에서 첨부파일들의 경로(자식)을 관리 할 때 유용.
    • 하지만, 해당 첨부파일들이 게시글 말고도 다른 엔티티들과 연관 관계가 있으면 절대 사용하면 안된다.
    • 단일 엔티티에 완전히 종속적인 Child를 관리할 때 유용하게 쓰자. 둘의 생명주기 (라이프 사이클)이 같기 때문에 가능하다.

CASCADE의 종류 (ALL, PERSIST, REMOVE만 주로 사용)

  • ALL - 모두 적용
    • 모든 라이프 사이클을 맞춰야 하거나 삭제가 위험할 때 
  • PERSIST - 영속
    • 저장할 때만 사용
  • REMOVE - 삭제
  • MERGE
  • REFRESH
  • DETACH

고아 객체

  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제한다.
  • 마찬가지로 @OneToMany Child 컬렉션 쪽에 orpahnRemoval = true 옵션을 준다.
@Entity
@Getter
@Setter
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Child> children = new ArrayList<>();

    public void addChild(Child child) {
        this.children.add(child);
        child.setParent(this);
    }
}
  • 자식 엔티티를 컬렉션에서 삭제하면
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);

em.flush();
em.clear();

Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildren().remove(0);

tx.commit();
  • 자동으로 삭제(DELETE)가 된다. 
...

Hibernate: 
    select
        parent0_.id as id1_8_0_,
        parent0_.name as name2_8_0_ 
    from
        Parent parent0_ 
    where
        parent0_.id=?
        
Hibernate: 
    select
        children0_.parent_id as parent_i3_2_0_,
        children0_.id as id1_2_0_,
        children0_.id as id1_2_1_,
        children0_.name as name2_2_1_,
        children0_.parent_id as parent_i3_2_1_ 
    from
        Child children0_ 
    where
        children0_.parent_id=?
        
Hibernate: 
    /* delete cascade.Child */ 
    delete 
        from
            Child 
        where
            id=?

고아 객체 사용 주의 사항

  • 이것 또한 마찬가지로 참조하는 곳이 하나일 때 사용해야 한다.
  • 특정 엔티티가 단도긍로 소유할 때 사용 가능하다. 여러 엔티티와 관계가 연관되어 있다면 사용하지 말자.
  • @OneToOne, @OneToMany -> @OneToXXX 에서만 사용 가능하다.
  • 참고
    • 개념적으로 부모를 제거하면 자식은 고아가 된다.
    • 따라서 고아 객체 기능을 활성화하면 부모를 제거하면 자식도 제거가 된다.
    • 이것은 cascade = CascadeType.ALL를 옵션에서 제거했지만 CascadeType.REMOVE 처럼 동작한다.

코드로 이해하기 ( child 대신 parent를 지운 경우 )

@Getter
@Setter
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<>();

    public void addChild(Child child) {
        this.children.add(child);
        child.setParent(this);
    }
}
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);

em.flush();
em.clear();

Parent findParent = em.find(Parent.class, parent.getId());
em.remove(findParent);

tx.commit();
  • delete 쿼리가 3개 나간다.
...

  Hibernate: 
    /* delete cascade.Child */ delete 
        from
            Child 
        where
            id=?
Hibernate: 
    /* delete cascade.Child */ delete 
        from
            Child 
        where
            id=?
Hibernate: 
    /* delete cascade.Parent */ delete 
        from
            Parent 
        where
            id=?

영속성 전이 + 고아 객체, 생명주기

  • CascadeType.ALL과 orphanRemoval = true를 같이 쓰면 어떻게 될까?
  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있따.
  • 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.

 

Reference

자바 ORM 표준 JPA 프로그래밍