본문 바로가기

Web Sever 개발과 CS 기초/스프링

JPA지연 로딩, 즉시 로딩 이해와 사용법 영속성전이, 고아 객체 알아보기

관련 내용

<JPA 영속성 컨텍스트부터 다양한 매핑까지 완전히 이해하기>

  • JPA 기본 동작과 영속성 원리 이해하기

JPA 기본 동작과 영속성 원리 이해하기

  • JPA 엔티티 매핑 - 테이블, 컬럼, 기본키

JPA 엔티티 매핑 - 테이블, 컬럼, 기본키

  • JPA 연관 관계 매핑과 고급 매핑

JPA 연관 관계 매핑과 고급 매핑

  • (현재 글)JPA지연 로딩, 즉시 로딩 이해와 사용법 영속성전이, 고아 객체 알아보기

JPA지연 로딩, 즉시 로딩 이해와 사용법 영속성전이, 고아 객체 알아보기

  • JPA 값 타입 이해하기

JPA 값 타입 이해하기

  • JPQL, fetch join 중심으로 이해하기

JPA JPQL, fetch join 중심으로 이해하기

개요 목적

이번 시간에는 참조된 필드를 연관된 테이블에서 바로 가져오지 않는 지연 로딩에 대해서 알아본다. 지연 로딩을 하게 되면, 연관된 테이블에 쿼리를 날리지 않기 때문에 성능 상 이점이 있다.

지연 로딩을 제대로 사용하려면 , 지연 로딩 시 생성되는 프록시 객체에 대해서 먼저 알아야 한다.

해당 내용을 바탕으로 지연 로딩을 잘 활용할 수 있는 방법을 알아보자.

마지막으로는 영속성 전이와 고아 객체에 대해서도 알아본다.

지연 로딩 이해하고 활용하기

지연 로딩에 대한 이해

지연 로딩을 이해하기 위해, 예시를 하나 들어보자.

아래 그림에 Member에서 Team을 참조할 수 있는 연관 관계가 매핑 되었다. 그래서 Member를 DB에서 조회하면, 자동으로 멤버에 해당하는 팀의 정보도 요청하게 된다.

그런데, 어떤 프로젝트에는, 멤버를 정보를 가져왔을 때, 팀 참조를 사용하지 않는다면, 멤버 조회하는데 팀 정보까지 가져오면, 낭비이지 않을까?

이런 참조 연관관계 필드 정보를 조회할 때 함께 가져올지 말지 결정하는 것이 지연 로딩이다.

지연 로딩을 구성하는 방법은 해당 엔티티의 (가짜)프록시 객체를 만드는 것이다. 먼저 프록시를 이해 한 후에, 지연 로딩과 즉시 로딩에 대해서 알아보자.

프록시 이해하기.

em.find(Member.class, id)를 하면 데이터베이스에서 조회해서, 정보가 들어 있는 실제 객체를 전달하지만,

em.getReference(Member.class, id)를 통해서 객체를 반환하면, DB 조회하지 않은 값이 텅빈 프록시 객체를 가져온다.

프록시 객체는 실제 클래스를 상속 받아서 만들어지고, 겉 모양은 같지만, 속이 텅 비어 있다.

내부에 실제 객체(taget)를 참조하고 있어서, 초기화를 하게 되면(initailize를 직접하거나, 내부 값을 조회하면), DB에서 데이터 조회 후 값이 있는 실제 엔티티와 연결을 한다.

그래서 프록시 객체.getName()을 하면 실제 name 값을 얻게 되는 것이다.

Member member = new Member();
member.setName("member1");
em.persist(member);
em.flush();
em.clear();

//일반 객체가 아닌 프록시 객체 생성
Member referenceMember = em.getReference(Member.class, member.getId());
//getName을 하는 순간 초기화 되어 안에 있는 target과
//실제 엔티티를 연결해서 값을 조회한다.
String name = referenceMember.getName();
System.out.println("name  :"+ name);
//Member$HibernateProxy$cBZE8ZN4 초기화를 해도 프록시 객체가 나온다.
System.out.println("reference className: " + referenceMember.getClass().getName());

tx.commit();

프록시의 특징

//1. 프록시 객체를 초기화 하면 실제 엔티티로 바뀌는 것이 아니라 
//프록시 객체를 통해서 엔티티에 접근
Member referenceMember = em.getReference(Member.class, member.getId());
System.out.println("before reference className: " + referenceMember.getClass().getName());
String name = referenceMember.getName();
//Member$HibernateProxy$cBZE8ZN4 초기화를 해도 프록시 객체가 나온다.
System.out.println("after reference className: " + referenceMember.getClass().getName());

//2. 타입을 비교할 때는 == 비교가 아닌, 같은 부모 클래스인 instanceOf 사용해야한다
Member member1 = new Member();
member1.setName("member1");
Member member2 = new Member();
member2.setName("member2");
em.persist(member1);
em.persist(member2);
em.flush();
em.clear();

Member realEntity = em.find(Member.class, member1.getId());
Member proxyEntity = em.find(Member.class, member2.getId());
//== 비교 : false  가 나온다. 하나는 실제 엔티티이고 하나는 프록시 객체이기 때문에
System.out.println("== 비교 : " + (realEntity == proxyEntity));

//3. 영속성 컨텍스트에 실제 엔티티가 있으면 getRefrence해도 실제 엔티티 반환
//(왜냐 하면 영속성 컨텍스트에 가져온 값들은 ==가 성립해야 하기 때문에
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1.getClass() = " + m1.getClass());
Member reference = em.getReference(Member.class, member1.getId());
//영속성 컨텍스트에 이미 실제 엔티티가 올라가 있으면 프록시 객체 조회해도 실제 객체 반환
//reference.getClass() = class hellojpa.Member
System.out.println("reference.getClass() = " + reference.getClass());
//그 반대의 경우 영속성컨텍스트에 프록시 객체가 있을 때 실제 엔티티 조회해도
//JPA == 맞추기 위해, 프록시 객체를 반환

//4.(중요!!)영속성 컨텍스트 도움을 받을 수 없는 경우에 초기화하면
//e.LazyInitializationException 예외를 터트림
Member reference = em.getReference(Member.class, member1.getId());
//영속성 컨텍스트 종료
em.close();
//영속성 컨텍스트에 프록시 객체 없는데 초기화 요청
//org.hibernate.LazyInitializationException 발생
reference.getName();

프록시 확인 메소드

  • 프록시 인스턴스의 초기화 여부 확인 PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법 entity.getClass().getName() 출력(..javasist.. orHibernateProxy…)
  • 프록시 강제 초기화 org.hibernate.Hibernate.initialize(entity);
  • 참고: JPA 표준은 강제 초기화 없음(하이버 네이트 구현체에만 있다.) 강제 호출: member.getName()

프록시 이해 바탕으로 지연 로딩, 즉시 로딩 사용하기

이제 처음 제시한 상황, 멤버를 조회하는데 팀 참조 데이터는 조회 안 하고 싶을 때 지연 로딩 사용 법을 알아보자.

지연 로딩 사용

@Entity
@Table
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    @Column(name = "USERNAME")
    private String name;
    //해당 필드에 fetch LAZY로 설정
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
 }

사용 결과

해당 Team 필드를 Lazy로 설정하면 지연 로딩이 실행되어,

team을 프록시 객체로 생성한다. team에 대한 정보를 조회(초기화)를 해야지만, team 정보를 DB에 요청한다.

Member member = em.find(Member.class, member1.getId());
//member는 hellojpa.Member 실제 객체
System.out.println("member.getClass().getName() = " + member.getClass().getName());
Team proxyTeam = member.getTeam();
//팀은 Team$HibernateProxy$eK855HTj 객체로 반환
System.out.println(
    "proxyTeam.getClass().getName() = " + proxyTeam.getClass().getName());
//팀에 대한 초기화를 해야지만, DB에 팀 관련 쿼리가 나간다.
proxyTeam.getName();
//member 조회할 때 Member에 대한 쿼리만 발생
Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.USERNAME as USERNAME2_0_0_,
        member0_.TEAM_ID as TEAM_ID3_0_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
//team.getName() 초기화를 하면 팀에 대한 쿼리 발생
Hibernate: 
    select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.id=?

즉시 로딩 사용

아래와 같이 즉시 로딩을 설정하면 member를 조회할 때 팀의 정보도 조인이 되어 member 객체를 만들어 준다.

@Entity
 public class Member {
 @Id
 @GeneratedValue
 private Long id;
 @Column(name = "USERNAME")
 private String name;
 @ManyToOne(fetch = FetchType.EAGER) //**
 @JoinColumn(name = "TEAM_ID")
 private Team team;
 }
select
        member0_.id as id1_0_0_,
        member0_.USERNAME as USERNAME2_0_0_,
        member0_.TEAM_ID as TEAM_ID3_0_0_,
        team1_.id as id1_1_1_,
        team1_.name as name2_1_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.id 
    where
        member0_.id=?

즉시 로딩을 사용시 주의사항

  • 예상치 못한 SQL이 발생한다.
  • Member에 FK가 여러 개일 경우 EAGER로 선택하면 엄청나게 조인된 쿼리를 날려서 DB에 요청하게 된다.
  • JPQL에서는 N+1 문제를 일으킨다
  • em.find의 경우 EAGER를 설정하면 알아서 JOIN으로 쿼리를 날려주지만, JPQL 로 “SELECT m FROM Member m”을 하게 되면 join이 아닌 연관된 테이블 모두 select를 날려서 데이터를 가져오게 된다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩-> LAZY로 설정하기
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

영속성 전이(cascade)

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용하는 옵션이다.

아래 예시 상황을 보고 이해해보자.

public class Parent{
    //이부분에 추가
    @OneToMany(mappyBy = "parent", cascade = CascadeType.ALL)
    private List<Child> childList = new ArrayList<>();
    
    public void addChild(child child){
       childList.add(child);
       child.setParent(this);
    }
}

public class Child{
    ...
    
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

main{
    try{
        Child child1 = new Child();
        Child child2 = new Child();
        
        Parent parent = new Parent();
        parent.addChild(child1);
        parent.addChild(child2);
        //parent만 영속성에 추가하면, child도 영속성에 추가된다.
        em.persist(parent);
    }
}

CASCADE의 종류

  • ALL: 모두 적용 (저장, 삭제 등등 라이프 사이클을 전부 맞춰야할 때)
  • PERSIST: 영속 (저장 할 때만)
  • REMOVE: 삭제 (삭제 할 때만)

사용 주의 사항

단일 소유자일 경우에만 사용한다.

(오직 parent만이 child를 소유할 때, 다른 Entity가 child를 소유해서는 안됨)

오직 하나의 parent가 child를 관리하는 거라면 CASCADE를 사용해도 상관없다.

그런데 child가 다른 Entity와 연관 관계를 맺고 관리되고 있다면 절대 사용하면 안된다.

고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 DB에서 자동으로 삭제 (Parent 객체에서 관리되는 컬렉션(childList)에서 제거가 되면 DB에서도 삭제가 된다.)

public class Parent{
    //orphanRemoval = true 옵션을 주면 적용이 된다. 
    @OneToMany(mappyBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Child> childList = new ArrayList<>();
}

main{
    Parent findParent = em.find(Parent.class, parent.getId());
    //부모에서 제거하면 DB에서 삭제된다. 
    findParent.getChildList().remove(0);
}