프록시와 연관 관계 관리

뚝딱이·2022년 9월 17일
0

JPA

목록 보기
7/11

왜 프록시를 사용할까 ?

위의 그림과 같은 상황일 때 Member를 조회한다면 DB에서 Team도 같이 조회해야 되는 걸까 ?

Member member = em.find(Member.class, 1L);
printMemeberAndTeam(member);

public void printMemeberAndTeam(Member member){
	String username = member.getUsername();
    System.out.println("username="+username);
    
    Team team = member.getTeam();
    System.out.println("team="+team.getName());
}

member에서(find) 쿼리가 나갈때 team도 가져오면 print할 때 한꺼번에 할 수 있다.

그런데 printMemberAndTeam()이 아닌 printMember라고 생각해보자.

Member member = em.find(Member.class, 1L);
printMemeber(member);

public void printMemeber(Member member){
	String username = member.getUsername();
    System.out.println("username="+username);
    
}

즉, 위와 같다. 이렇게 되면 Team을 사용하지도 않는데 member를 조회할 때 team까지 가져오는 건 좋지 않다. 낭비가 되기 때문이다.

따라서 경우에 따라 Team 정보를 함께 가져오는게 좋기도, 함께 가져오지 않는게 좋기도 하다.

프록시란 ?

em.find()말고도 em.getReference()라는 메서드가 있다. 말그대로 참조를 가져오는 것이다.
둘의 차이가 무엇일까.

  • em.find() : 데이터베이스를 통해서 실제 엔티티 객체를 조회한다.
  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다. DB에 쿼리가 안나가는데 조회가 된다. 예제로 알아보자.

Member member = new Member();
member.setUsername("hello");

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());

위를 실행하면, JPA가 Join을 해서 Member와 Team을 함께 조회하는 것을 알 수 있다.

Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.TEAM_ID as TEAM_ID3_3_0_,
        member0_.USERNAME as USERNAME2_3_0_,
        team1_.TEAM_ID as TEAM_ID1_5_1_,
        team1_.name as name2_5_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
findMember.id=1
findMember.username=hello

그런데 만약 em.find말고 getReference를 사용하면 어떻게 될까.


Member member = new Member();
member.setUsername("hello");

em.persist(member);

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

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

위를 실행하면 분명히 getReference를 했는데도 select쿼리가 나가지 않는다.


Member member = new Member();
member.setUsername("hello");

em.persist(member);

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

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

하지만 찾은 멤버(findMember)의 id와 이름을 조회해서 출력해보면 select쿼리가 나가는 것을 확인할 수 있다.

즉, em.getReference()를 하는 시점에선 데이터베이스에 쿼리를 날리지 않는다.
실제로 em.getReference()를 해서 찾은 객체를 사용할 때 쿼리가 나간다.

그렇다면 findMember는 무엇일까 ?

System.out.println("findMember = " + findMember.getClass());를 통해 findMember를 출력해보면 findMember = class hellojpa.Member$HibernateProxy$lURU8Jb1가 출력된다. 보면 그냥 Member 클래스가 아니라 뒤에 HibernateProxy가 붙어있다. 즉, 이 클래스는 Hibernate가 만들어낸 가짜 클래스인 것이다.

프록시는 껍데기는 진짜 클래스와 같은데 내부가 텅텅빈 것이다.
내부엔 target이라는 것이 있는데 이 target이 진짜 reference를 가리킨다.

프록시 특징

실제 클래스를 상속 받아서 만들어지기 때문에 실제 클래스와 겉 모양 (껍데기)이 같다.
그래서 사용하는 입장에선 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다. (이론상)

프록시 객체는 target이라는 실제 객체의 참조를 보관한다.
따라서 프록시 객체를 호출하면 프록시 객체에 있는 target, 즉 실제 객체의 메서드를 호출한다.

처음엔 DB에서 조회한적이 없기 때문에 Proxy의 target은 null이다.

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

의 상황에서 프록시 객체가 어떻게 사용되는지 알아보자.

getReference로 프록시 객체를 가져온다.
member.getName()을 하면 프록시의 target값이 없으니 영속성 컨텍스트에 이 값을 가져올 것을 요청한다. (초기화 요청)
그럼 이 때 영속성 컨텍스트에서 DB를 조회해서 실제 Entity를 생성한다.
Entity가 생성되면 target을 통해 Name을 가져온다.

한번 초기화 하면 target이 지정되기 때문에 다음엔 초기화할 필요가 없다.

즉, 프록시 객체는 처음 사용할 때 한 번만 초기화한다. 두번, 세번 해도 초기화 안된다.

프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다. 초
기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능해 프록시 객체를 통해서 실제 엔티티에 접근한다. 헷갈릴 수 있으므로 유의하자.

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

를 하면 처음 getClass를 했을 땐 target이 비어있는 상태다. 하지만 두번 째 getClass를 하면 getUsername에서 초기화 요청이 진행되었으므로 target을 지정했다. 그렇다면 findMember는 이제 진짜 Member 클래스인가 ? 아니다. 출력된 것을 보면 똑같이 프록시 객체이다. 이를 통해 target이 지정되어도 진짜 클래스로 바로 접근하는 것이 아닌 프록시 객체를 거친다는 것을 알 수 있다.

프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야한다. (== 비
교 실패, 대신 instance of 사용) JPA에서 엔티티를 비교할 일이 있으면 ==비교 하지말고 instance of를 사용하도록 하자.

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

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

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

로 member1과 member2가 있다. 이제 member1과 member2를 찾아 == 비교를 해보자.

            Member m1 = em.find(Member.class, member1.getId());
            Member m2 = em.find(Member.class, member2.getId());

            System.out.println("m1 == m2" + (m1.getClass() == m2.getClass()));

find를 이용해 조회했다. 이 경우엔 true가 나오는데, 이는 find를 사용하면 DB에 직접 조회하기 때문이다.
그렇다면 getReference를 사용하면 어떻게 될까.


            Member m1 = em.find(Member.class, member1.getId());
            Member m2 = em.getReference(Member.class, member2.getId());

            System.out.println("m1 == m2" + (m1.getClass() == m2.getClass()));

member1은 그대로 find를 사용했지만 member2는 getReference를 사용해 조회했다. 당연하게도 false가 출력된다. 실무에선 프록시로 넘어오는지 진짜로 넘어오는지 모르기 때문에 이렇게 == 비교를 해서는 안된다. instance of를 사용하면 프록시는 진짜 클래스를 상속한것이기 때문에 가능하다. 따라서 instance of를 무조건 사용하도록 하자

            Member m1 = em.find(Member.class, member1.getId());
            Member m2 = em.getReference(Member.class, member2.getId());

            System.out.println("m1 == m2" + (m1 instanceof Member));
            System.out.println("m1 == m2" + (m2 instanceof Member));

둘 다 true가 출력된다.

            Member m1 = em.find(Member.class, member1.getId());
            System.out.println("m1 = " + m1.getClass());

            Member reference = em.getReference(Member.class, member1.getId());
            System.out.println("reference = " + reference.getClass());

의 경우에 reference의 클래스는 프록시일까 진짜 Member일까.
출력해보면 진짜 클래스인 Member가 찍힌다.

이는 두가지 이유가 있다.

  1. 이미 find에서 Member를 영속성 컨텍스트에, 즉 1차 캐시에 넣어놓은 상태이다. 따라서 이것을 프록시로 가져와봤자 아무 이점이 없다.
  2. JPA에서는 한 영속성 컨텍스트에서 가져왔고, pk가 같은 경우 ==비교를 했을 때 무조건 true를 반환해야한다.

따라서 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환한다.

            Member m1 = em.getReference(Member.class, member1.getId());
            System.out.println("m1 = " + m1.getClass());

            Member reference = em.getReference(Member.class, member1.getId());
            System.out.println("reference = " + reference.getClass());

m1과 reference 모두 getReference로 조회하면 어떻게 될까.

m1 = class hellojpa.Member$HibernateProxy$6AV401GM
reference = class hellojpa.Member$HibernateProxy$6AV401GM

같은 프록시 객체가 반환되는 것을 볼 수 있다. 이는 ==비교가 가능해야하기 때문에 같은 프록시 객체가 반환된 것이다.

            Member refMember = em.getReference(Member.class, member1.getId());
            System.out.println("m1 = " + refMember.getClass());

            Member findMember = em.find(Member.class, member1.getId());
            System.out.println("reference = " + findMember.getClass());

위와 같이 getReference를 한 다음 find를 하면 어떻게 될까.
refMember는 당연히 프록시를 가져온다. 그리고 find를 하면서 쿼리를 날리지만 findMember에는 프록시가 온다. refMember와 같은 프록시이다. ==비교를 맞춰야하기 때문이다.

따라서 진짜 클래스를 조회하면 다음 조회때도 진짜 클래스가 조회되고, 프록시를 먼저 조회하면 다음에도 프록시가 나온다.

            Member refMember = em.getReference(Member.class, member1.getId());
            System.out.println("m1 = " + refMember.getClass());

            em.detach(refMember);//영속성 컨텍스트에서 더이상관리 안해
            refMember.getUsername();//쿼리가 나가면서 초기화 요청됨. 이때 초기화는 영속성 컨텍스트를 통해 실행된다.

영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다. detach가 아닌 clear와 close의 경우에도 똑같다.
(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인: PersistenceUnitUtil.isLoaded(Object entity)
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println(emf.getPersistenceUnitUil().isLoaded(refMember));

를 하면 아직 프록시는 초기화 되지 않았으므로 false가 출력된다.

Member refMember = em.getReference(Member.class, member1.getId());
refMember.getUsername();
System.out.println(emf.getPersistenceUnitUil().isLoaded(refMember));

getUsername()에서 프록시를 초기화하므로 true가 출력된다.

  • 프록시 클래스 확인 방법
    entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)

  • 프록시 강제 초기화
    org.hibernate.Hibernate.initialize(entity);

  • 참고: JPA 표준은 강제 초기화 없음
    강제 호출: member.getName()

즉시 로딩과 지연 로딩

지연 로딩

그럼 처음으로 돌아가서 결국 Member를 조회할 때 Team도 함께 조회해야할까 ?
Member의 정보만 필요할 땐 Team까지 함께 조회할 필요가 없다.

이를 위해 JPA는 지연로딩을 지원한다.

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id; //PK

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때


    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

ManyToOne에 fetch를 추가해 타입을 LAZY로 지정하자. 그러면 Team을 프록시 객체로 조회한다. 즉 Member 클래스만 db에서 조회하는 것이다.

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

로 조회를 하면 이전에는 Team까지 Join해서 모두 조회했었다. 하지만 Fetch를 지정한 지금 어떻게 될까.

Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.TEAM_ID as TEAM_ID3_3_0_,
        member0_.USERNAME as USERNAME2_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?

member만 조회하는 것을 볼 수 있다.

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

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

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

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

member의 team의 클래스를 출력해보면 프록시 객체인 것을 볼 수 있다.

findMember = class hellojpa.Team$HibernateProxy$EEZpbzRK
            Member findMember = em.find(Member.class, member1.getId());
            System.out.println("findMember = " + findMember.getTeam().getClass());
            findMember.getTeam().getName();

위와 같이 team을 건드려 프록시가 초기화 되면 team에 대한 쿼리가 나가는 것을 확인 할 수 있다. 즉 team을 사용하는 시점에 쿼리가 나간다.

즉 지연로딩을 걸어주면, member를 조회해도 team은 프록시로 조회된다.

위의 상황에선 member의 정보만 필요한 경우가 많았다. 하지만 member와 team이 같이 사용할 일이 많다면 어떻게 해야될까.

즉시 로딩

member와 team이 같이 사용할 일이 많다면 즉시 로딩 EAGER를 사용해서 함께 조회하면 된다.

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id; //PK

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

fetch 타입을 EAGER로 변경해주자.

그리고 실행해보면, Member와 Team을 Join해서 한번에 조회한다.
따라서 Team도 프록시가 아닌 진짜 클래스를 가져온다. 따라서 초기화 요청이 필요없다.

Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.TEAM_ID as TEAM_ID3_3_0_,
        member0_.USERNAME as USERNAME2_3_0_,
        team1_.TEAM_ID as TEAM_ID1_5_1_,
        team1_.name as name2_5_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?

즉, member가 로딩 될 때 team도 같이 로딩된다.

대부분의 JPA 구현체는 가능하면 조인을 사용해 SQL을 한번에 함께 조회하려고 한다. (MEMBER따로 TEAM따로가 아닌)

프록시와 즉시로딩 주의

실무에선 가급적이면 지연로딩만 사용하는게 좋다.
왜일까 ?

  1. 예상치 못한 SQL이 발생한다.
    em.find로 Member를 가져오는데, team을 가져오는 쿼리도 나간다.
    테이블이 2개가 아닌 10개가 엮여있다고 생각해보자. 그럼 join도 그만큼 해야하는데, 이는 예상치 못한 sql이 된다.

  2. 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.

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

위를 실행하면 쿼리가 두번 나가는 것을 볼 수 있다.

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.MEMBER_ID as MEMBER_I1_3_,
            member0_.TEAM_ID as TEAM_ID3_3_,
            member0_.USERNAME as USERNAME2_3_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.TEAM_ID as TEAM_ID1_5_0_,
        team0_.name as name2_5_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?

분명히 fetch를 EAGER로 지정했는데, 왜 두번 나가는 것일까
em.find는 pk를 찍어서 가져오는 것이기 때문에 JPA가 내부적으로 최적화할 수 있다.
하지만 JPQL은 그대로 SQL로 번역된다. 이는 MEMBER만 SELECT한다. 근데 MEMBER를 가져오면 TEAM이 즉시로딩이면 MEMBER에 TEAM이 들어가있어야한다. 따라서 쿼리가 별도로 나가는 것이다.


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

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

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

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

위처럼 team이 여러개 이고 각각의 member의 team이 다르다고 가정해보자.
그럼 쿼리는 몇번 나갈까

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.MEMBER_ID as MEMBER_I1_3_,
            member0_.TEAM_ID as TEAM_ID3_3_,
            member0_.USERNAME as USERNAME2_3_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.TEAM_ID as TEAM_ID1_5_0_,
        team0_.name as name2_5_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
Hibernate: 
    select
        team0_.TEAM_ID as TEAM_ID1_5_0_,
        team0_.name as name2_5_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?

처음 select * from member로 member에 대한 쿼리가 한번 나가고, member마다 team이 달라 team에 대한 쿼리가 2번 나간다.

처음 쿼리 1개를 날렸을 때 이 쿼리 때문에 나가는 추가 쿼리를 N이라고 한다. 따라서 N+1이라 한다.

따라서 LAZY로 잡아야한다. LAZY로 지정하면 어떻게 될까

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.MEMBER_ID as MEMBER_I1_3_,
            member0_.TEAM_ID as TEAM_ID3_3_,
            member0_.USERNAME as USERNAME2_3_ 
        from
            Member member0_

쿼리는 한번만 나간다.

근데 생각해보면 지연로딩도 나중에 TEAM을 조회하면 그것대로 쿼리가 많이 나온다.
이에 대한 대안은 세가지가 있다.
일단 모두 지연로딩으로 설정한다.
1. FETCH JOIN : 런타임에 동적으로 가져올 애들을 선택해서 그것들만 가져오는 것
Member만 필요할 땐 member만 가져오고 team도 필요할 땐 fetch join을 이용해 member와 team을 가져오도록 한다.
select m from Member m join fetch m.team으로 JPQL을 설정하면 Member와 team을 모두 가져온다.
2. 엔티티 그래프
3. batch size

보통 FETCH JOIN으로 거의 다 해결한다.

@ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 모두 직접 LAZY로 지정해줘야한다.
@OneToMany, @ManyToMany는 기본이 지연 로딩이다.

영속성 전이 : CASCADE

영속성 전이란, 부모를 저장할 때 자식도 같이 저장하고 싶을 때 처럼 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다.

지연로딩이나 즉시로딩, 연관관계 매핑등과 아무 관계가 없다.

parent와 child를 만들어보자.

@Entity
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

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

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }
}
@Entity
public class Child {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
    public Long getId() {
        return id;
    }
}

그렇다면 우리는 parent와 child를 만들어 영속 상태로 만들고 싶을 때 각각 persist해야한다.

            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);

굉장히 번거롭다. parent 중심으로 코드를 짜고 싶어서 child가 자동으로 persist되길 바란다. 이때 사용하는 것이 cascade이다.

@Entity
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

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

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }
}

Parent에 cascade를 추가 했다.
그러면 이제 child는 따로 persist를 하지 않아도 된다.

            Child child1 = new Child();
            Child child2 = new Child();

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

            em.persist(parent);

까지만 해도 child들은 알아서 persist된다.

parent를 저장할 때 child들도 저장하고 싶을 때 사용한다. parent가 영속화 될 때 영속성 전이가 일어나 child들도 영속화 된다.

영속성 전이는 편리함 제공의 역할을 할 뿐 연관관계 매핑과는 아무관련도 없다.

종류

  • ALL: 모두 적용
  • PERSIST: 영속
  • REMOVE: 삭제
  • MERGE: 병합
  • REFRESH: REFRESH
  • DETACH: DETACH
    등이 있으나, 많이 사용되는 것은 ALL, PERSIST, REMOVE이다. 라이프사이클을 모두 맞춘다면 ALL을 사용한다.

주의할 점이 있다. 언제쓰느냐 ? 하나의 부모가 자식들을 관리할 때는 CASCADE가 의미있다. 게시판이랑 첨부파일을 예로 들때 한 게시판에서 여러 첨부파일을 관리하기 때문이다. 하지만 이 첨부파일이 여러 게시판에서 관리된다면 사용하면 안된다.

따라서 소유자가 하나일 때만 사용하자. Child가 parent 말고 다른것과 연관관계가 있을 경우 사용하면 안된다. 운영이 너무 힘들어지기 때문
완전히 종속적일 때 사용.

라이프 사이클이 유사할 때, 단일 소유자일 때 이 두가지 전제를 만족할 때 사용해야한다.

고아 객체

고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제

@Entity
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

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

    public void addChild(Child child) {
        childList.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.getChildList().remove(0);

를 하면 자식 엔티티를 컬렉션에서 삭제된다.

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 하나일 때 사용해야한다.
  • 특정 엔티티가 개인 소유할 때 사용한다.
  • @OneToOne, @OneToMany만 가능
  • 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다. REMOVE로 해놓으면 부모가 제거 될 때 영속성 전이를 통해 Child들도 다 remove 되기 때문이다.

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

CascadeType.ALL + orphanRemovel=true

  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있다.
  • 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용하다.

출처 : 자바 ORM 표준 JPA 프로그래밍 - 기본편

profile
백엔드 개발자 지망생

0개의 댓글