티스토리 뷰

1. 프록시

2. 즉시 로딩과 지연 로딩

3. 영속성 전이: CASCADE

4. 고아객체

5. cascade 와 orphanRemoval

 

 

 

1. 프록시

em.find() : 실제 엔티티 조회

em.getReference() : 프록시 객체 조회

 

1) em.getReference() 동작 과정

 

JPA 는 실제 엔티티(Memeber)를 상속받는 프록시 객체(MemberProxy)를 사용한다.

프록시 객체는 실제 객체의 참조(Memeber target)를 보관한다.

em.getReference() 를 통해 엔티티를 호출하면 프록시 객체를 반환하고

member.getName() 을 호출할 때 처럼 실제 엔티티 객체의 정보가 필요한 경우

영속성 컨텍스트에 Member target 의 초기화를 요청한다.

DB를 조회해 실제 엔티티 객체(member)를 초기화 하고

Member target 에도 참조값을 초기화 한다.

프록시 객체를 통해 데이터를 조회하면 실제 엔티티 객체의 메소드가 호출된다.

 

2) 프록시의 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 
  • 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
    (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)

 

JPA는 트랜잭션 내에서 객체간의 동일성을 보장하기 때문에

em.getReference(), em.find 를 통해 객체를 조회하면 먼저 조회한 객체(프록시 혹은 원본)가

이 후 조회할 때에도 동일하게 조회된다.

 

(1) 원본 엔티티 객체를 먼저 조회할 때

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

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

em.reference() 로 조회해도 원본 객체를 반환한다.

 

(2) 프록시 객체를 먼저 조회할 때

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

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

em.find() 로 객체를 호출해도 프록시 객체를 반환한다.

따라서, 객체 간의 타입을 비교할 때는 == 비교 대신 instance of 를 사용해야 한다.

(비교할 대상이 프록시 객체일 수 있음)

findMember instance of Member
referenceMember instance of Member

 

3) 프록시 확인

프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity) 
프록시 클래스 확인 방법 : entity.getClass().getName() 출력(..javasist.. or HibernateProxy…) 
프록시 강제 초기화 : org.hibernate.Hibernate.initialize(entity); 
참고: JPA 표준은 강제 초기화 없음 : 강제 호출: member.getName()

 

 

2. 즉시 로딩과 지연 로딩

Member - Team 엔티티의 연관관계가 ManyToOne 으로 매핑되어 있을 때

Member 를 조회하면 Team 도 함께 조회하도록 할지 (즉시로딩)

Team 의 데이터를 조회할 때 쿼리를 실행해서 Team 데이터를 조회할지 (지연로딩)

연관관계의 FetchType 으로 설정할 수 있다.

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Team {
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;
}

 

1) 지연 로딩

Member 를 조회하면 Team 을 프록시 객체로 설정한다.

Team 을 실제 사용하는 시점에 조회 쿼리를 실행하고 Team 프록시 객체를 초기화 한다.

@ManyToOne(fetch = FetchType.LAZY)

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Member extends BaseEntity {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String name;

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

Member member1 = new Member();
member1.setName("member1");
member1.setTeam(team1);

em.persist(team1);
em.persist(member1);

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

Member findMember1 = em.find(Member.class, member1.getId());
Team findTeam = findMember1.getTeam();

System.out.println("team = " + findMember1.getTeam().getClass());
System.out.println("team name = " + findTeam.getName());

findMember1.getTeam() 에서 조회되는 객체가 프록시 객체이고

findTeam.getName() 시점에 프록시 객체 초기화가 일어난다.

 

2) 즉시 로딩

비즈니스 로직에서 대부분 Member와 Team 을 함께 사용한다면

즉시 로딩으로 설정해 Member 가 조회될 때 Team 도 함께 조회할 수 있도록 설정.

@ManyToOne(fetch = FetchType.EAGER)

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Member extends BaseEntity {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String name;

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

Member member1 = new Member();
member1.setName("member1");
member1.setTeam(team1);

em.persist(team1);
em.persist(member1);

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

Member findMember1 = em.find(Member.class, member1.getId());
Team findTeam = findMember1.getTeam();

System.out.println("team = " + findMember1.getTeam().getClass());
System.out.println("team name = " + findTeam.getName());

em.find(Member.class, member1.getId()) 시점에 TEAM 을 조인해서 조회한다.

findTeam 객체는 프록시가 아닌 실제 객체이다.

 

3) 프록시와 즉시로딩 주의

  • 가급적 지연 로딩만 사용(특히 실무에서)
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

 

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

Team team1 = new Team();
team1.setName("team1");

Team team2 = new Team();
team2.setName("team2");

Team team3 = new Team();
team3.setName("team3");

Member member1 = new Member();
member1.setName("member1");
member1.setTeam(team1);

Member member2 = new Member();
member2.setName("member2");
member2.setTeam(team2);

Member member3 = new Member();
member3.setName("member3");
member3.setTeam(team3);

em.persist(team1);
em.persist(team2);
em.persist(team3);
em.persist(member1);
em.persist(member2);
em.persist(member3);

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

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

JPQL 로 Member List 를 조회하면 SELECT MEMBER 쿼리가 실행된다.

이 후 Team 엔티티와 즉시 로딩으로 설정되어 있기 때문에

리스트에 들어있는 모든 Member 의 Team(영속성 컨텍스트에 없는 데이터) 을 각각 DB에서 조회한다.

 

4) 지연 로딩 활용

비즈니스 로직에 따라 즉시로딩을 설정할 수 있지만 실무에서는 지연로딩을 사용하자.

 

 

결론

모든 연관관계를 지연 로딩으로 설정한다.

필요한 경우에만 JPQL의 fetch join 이나 엔티티 그래프를 사용하자.

 

 

3. 영속성 전이: CASCADE

연관관계 매핑과 관계없이 casecade 를 설정한 필드에 저장된 객체에 영향을 준다.

 

1) OneToMany 에 설정

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)

private List<Child> childList = new ArrayList<>();

 

Parent 클래스에서 childList 에 cascade 를 All 로 설정하면

Parent 객체를 영속화할 때 childList 에 저장되어 있는 객체들을 함께 영속화 하겠다는 의미.

 

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Child {
    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;
}
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Parent {
    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    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);
    }
}
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);

 

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

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

em.persist(parent);
em.remove(parent);

2) ManyToOne 에 설정

@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "PARENT_ID")
private Parent parent;

 

Child 객체를 영속화할 때 Child.parent 에 저장되어 있는 객체도 함께 영속화

PARENT 가 부모 테이블이기 때문에 먼저 insert

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Child {
    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;

    private String name;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;
}
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Parent {
    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    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);
    }
}
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);

 

3) CASCADE 유의사항

  • ALL: 모두 적용
  • PERSIST: 영속
  • REMOVE: 삭제
  • MERGE: 병합
  • REFRESH: REFRESH
  • DETACH: DETACH

Parent 와 Child 가 완전히 종속적인 경우, life cycle 이 동일한 경우, 단일 소유자(Parent 만 Child 를 소유할 때)인 경우

CASCADE 설정이 관리하기에 편리하지만 (ALL, PERSIST 정도 사용)

그렇지 않은 경우 사용하지 말자 

 

 

4. 고아객체

참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능

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

 

@OneToMany(mappedBy = "parent", orphanRemoval = true)

private List<Child> childList = new ArrayList<>();

 

childList 에서 remove 되는 객체를 DB 에서 delete 되도록 설정

CascadeType.REMOVE처럼 동작

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Parent {
    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", 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(child1);
em.persist(child2);
em.persist(parent);
em.flush();
em.clear();

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

findParent.getChildList().remove(0) 시점에 고아 객체가 DB에서 delete 되어야 하지만

실제 delete 쿼리가 실행되지 않음.

해당 내용을 검색해보니 하이버네이트 구현체에서는 이 동작에 버그가 있다고 한다.

참고

 

@OneToMany(mappedBy = "parent", orphanRemoval = true)

orphanRemoval 설정만으론 동작하지 않고

CascadeType.ALL 이나 CascadeType.PERSIST 를 함께 설정해 주어야 정상 동작한다.

하지만 실무에서 orphanRemoval 만 따로 적용하는 경우는 거의 없기 때문에 문제가 되진 않는다고 한다.

 

5. cascade 와 orphanRemoval

@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST, orphanRemoval = true)

private List<Child> childList = new ArrayList<>();

 

Child 가 Parent 하나에만 종속적일 때(개인 소유)와 생명주기가 거의 같을때 사용하고

Parent 를 영속화 하면 childList 에 저장된 객체들을 함께 영속화 한다.

childList 에서 객체를 제거하면 고아 객체가 되기 때문에 DB 에서 삭제된다.

>> 부모 엔티티를 통해 자식 엔티티의 생명주기를 관리한다.

 

 

 

 

 

출처

https://www.inflearn.com/course/ORM-JPA-Basic 자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한)

728x90

'ORM > JPA' 카테고리의 다른 글

[JPA] 8. JPQL(1)  (0) 2022.03.29
[JPA] 7. 값 타입  (0) 2022.03.02
[JPA] 5. 고급 매핑  (0) 2021.12.01
[JPA] 4. 다양한 연관관계 매핑  (0) 2021.11.29
[JPA] 3. 연관관계 매핑  (0) 2021.11.09