제육's 휘발성 코딩
반응형

목차

  • 프록시
  • 즉시 로딩과 지연 로딩
  • 지연 로딩 활용
  • 영속성 전이 : CASCADE
  • 고아 객체
  • 영속성 전이 + 고아 객체, 생명주기
  • 예제

프록시

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Member member = em.find(Member.class, 1L);
            printMember(member);
//            printMemberAndTeam(member);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

    private static void printMember(Member member) {
        System.out.println("member = " + member.getName());
    }

    private static void printMemberAndTeam(Member member) {
        String userName = member.getName();
        System.out.println("userName = " + userName);

        Team team = member.getTeam();
        System.out.println("team = " + team.getName());
    }
}
  • 멤버만 조회하려고할 때 팀도 조회해야할까?

프록시 기초

image

  • em.find() : 데이터베이스를 통해 실제 엔티티 객체 조회
  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회

프록시 특징

  • 프록시는 실제 클래스를 상속 받아 만들어지며, 겉 모양이 같다.
  • 이론상으로 사용하는 입장에서는 진짜인지 프록시 객체인지 구분하지 않고 사용하면 된다.
  • 한 트랜잭션 안에서 객체의 동일성을 보장 (프록시, 실제 객체 구별없이 동일성 보장 - 컬렉션 방식)

프록시 객체 초기화

image

  • getReference()를 사용해서 조회하려는 값이 프록시가 갖고 있지 않은 값이면 영속성 컨텍스트에 초기화 요청을 하여 실제 객체에 값을 넣어준다.
package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Member member = new Member();
            member.setName("hello");

            em.persist(member);

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

//            Member findMember = em.find(Member.class, member.getId());
            Member findMember = em.getReference(Member.class, member.getId()); // 프록시 객체 생성 
            System.out.println("findMember = " + findMember.getClass()); // ID는 갖고 있으므로 프록시에서 바로 반환 
            System.out.println("findMember.id = " + findMember.getId()); // ID는 영속성 컨텍스트에 초기화 요청을 통한 DB접근 
            System.out.println("findMember.userName() = " + findMember.getName());
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • 프록시의 동작과정을 다음과 같이 확인해볼 수 있다. getClass를 찍어보면 프록시 클래스임을 알 수 있다.

프록시 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Member member1 = new Member();
            member1.setName("member1");
            em.persist(member1);

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

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

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

            System.out.println("m1  : " + (m1 instanceof Member));
            System.out.println("m2  : " + (m2 instanceof Member));
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능한 것이다. 즉, 타입 체크시 == 가 아닌 상속관계인 instanceof를 사용해야한다.
package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Member member1 = new Member();
            member1.setName("member1");
            em.persist(member1);

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

            Member m1 = em.find(Member.class, member1.getId());
            System.out.println("m1  : " + (m1.getClass()));
                        // 영속성 컨텍스트에 m1에 대한 정보가 등록되어있다. 
            Member reference = em.getReference(Member.class, member1.getId());
            System.out.println("reference : " + (reference.getClass()));
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

    private static void printMember(Member member) {
        System.out.println("member = " + member.getName());
    }

    private static void printMemberAndTeam(Member member) {
        String userName = member.getName();
        System.out.println("userName = " + userName);

        Team team = member.getTeam();
        System.out.println("team = " + team.getName());
    }
}
  • 영속성 컨텍스트에 찾는 엔티티가 이미 존재하면, 프록시를 생성해도 실제 엔티티를 반환한다.
  • 반대로 프록시를 먼저 생성하고 실제 객체를 생성하려고해도 프록시를 반환한다. 즉, 트랙잭션 안에서 객체의 동일성을 보장한다.
package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Member member1 = new Member();
            member1.setName("member1");
            em.persist(member1);

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

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

            em.detach(refMember); // 준영속 상태로 만듦

            refMember.getName(); // 예외 발생 

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태(detach, clear, close 등)일 때, 프록시를 초기화하면 문제가 발생한다. 프록시는 한 번만 초기화된다.
  • LazyInitializationException 예외가 발생한다.

프록시 확인

Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember  : " + (refMember.getClass()));
refMember.getName(); // 프록시 없으니 초기화 요청
System.out.println("isLoaded= " + emf.getPersistenceUnitUtil().isLoaded(refMember));
  • 프록시 인스턴스의 초기화 여부 확인 (emf.getPersistenceUnitUtil().isLoaded(refMember))
  • 프록시 클래스 확인 (refMember.getClass())
  • 프록시 강제 초기화
    • getName()과 같은 것이 강제 초기화 방식이다.
    • 하이버네이트는 Hibernate.initialize(refMember)가 있지만, JPA는 강제 초기화가 없어서 getName()과 같은 방식을 사용한다.

즉시로딩과 지연 로딩

회원 엔티티에 지연 로딩 적용

package hellojpa;

import javax.persistence.*;

@Entity
public class Member extends BaseEntity {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private Team team;
}
  • 연관관계에 fetch = FetchType.LAZY 로 지연 로딩을 적용 시켜보자.
package hellojpa;

import javax.persistence.*;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Team team = new Team();
            team.setName("teamA");
            em.persist(team);

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

            em.persist(member1);

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

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

            System.out.println("===========");
            m.getTeam().getName();
            System.out.println("===========");

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • 지연로딩을 사용하면 프록시를 사용해서 연관된 테이블이 필요할 때만 쿼리를 날린다고 했다. 테스트를 통해 검증해보자.
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_3_0_,
        member0_.createDate as createda2_3_0_,
        member0_.createdBy as createdb3_3_0_,
        member0_.lastModifiedDate as lastmodi4_3_0_,
        member0_.modifiedBy as modified5_3_0_,
        member0_.USERNAME as username6_3_0_,
        member0_.team_TEAM_ID as team_tea7_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
member : class hellojpa.Member
member.team = class hellojpa.Team$HibernateProxy$D9znehjT
===========
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_7_0_,
        team0_.createDate as createda2_7_0_,
        team0_.createdBy as createdb3_7_0_,
        team0_.lastModifiedDate as lastmodi4_7_0_,
        team0_.modifiedBy as modified5_7_0_,
        team0_.name as name6_7_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
===========
  • 지연로딩이 정상적으로 적용된 결과를 얻을 수 있다. 즉, Team에 관련된 정보가 없으면 실제 객체를, 있으면 프록시 객체를 통해 조회 한다.

즉시 로딩 적용

Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_3_0_,
        member0_.createDate as createda2_3_0_,
        member0_.createdBy as createdb3_3_0_,
        member0_.lastModifiedDate as lastmodi4_3_0_,
        member0_.modifiedBy as modified5_3_0_,
        member0_.USERNAME as username6_3_0_,
        member0_.team_TEAM_ID as team_tea7_3_0_,
        team1_.TEAM_ID as team_id1_7_1_,
        team1_.createDate as createda2_7_1_,
        team1_.createdBy as createdb3_7_1_,
        team1_.lastModifiedDate as lastmodi4_7_1_,
        team1_.modifiedBy as modified5_7_1_,
        team1_.name as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.team_TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
member : class hellojpa.Member
member.team = class hellojpa.Team
===========
===========
  • 즉시로딩 (EAGER) 방식은 Member 가져올 때 연관 된 모든 테이블을 가져온다.

지연로딩과 즉시로딩의 결정 여부는 비즈니스 로직에 따라 다르다. Member 와 Team을 같이 조회하는 경우가 많으면 즉시로딩(EAGER)을 Member 만 조회하는 경우가 많으면 지연로딩(LAZY)을 사용하는 것이 바람직하다. 하지만 즉시로딩 시 주의할 점이 존재해서 실무에서는 즉시로딩을 사용하지 않는다.

즉시로딩 주의

  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생 (테이블이 많을 수록 더 문제가 생긴다.)
package hellojpa;

import javax.persistence.*;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Team teamA = new Team();
            teamA.setName("teamA");
            em.persist(teamA);

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

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

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

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

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

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


            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. N+1이란 즉시 로딩에서 쿼리를 1번 날렸지만, 값을 가져오기위해 N번이 추가적으로 실행되어야하는 문제를 말한다. JPQL에서 쿼리문은 SQL로 번역되어 해당 Member 엔티티를 조회하는 쿼리를 날린다. 이 때 변환 과정에서 즉시로딩인 것을 보고 다시 팀에 관련된 쿼리를 다시 날리게 된다.

N+1 문제를 해결하기 위해선 우선적으로 모든 연관관계를 지연로딩으로 만든다. 이후 3가지 방법이 있다.

첫 번째로는 JPQL의 fetchJoin이 있다. fetchJoin은 런타임에 동적으로 원하는 객체를 가져오는 것이다. List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class).getResultList(); 와 같이 사용한다.

두 번째로는 엔티티 그래프와 어노테이션으로 해결하는 방법이 있다.

세 번째로는 배치사이즈를 사용해서 해결하는 방법이 있다. (1+1 방법)

  • @ManyToOne, @OneToOne 은 기본이 즉시 로딩 (LAZY 설정 필요)
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

지연 로딩 활용

image

  • 이론적이지만, 객체간 사용여부에 따라 즉시 로딩과 지연 로딩을 나누어서 설계할 수 있다. 실무에서는 지연 로딩만 사용하므로 가볍게 알아두자.

영속성 전이 : CASCADE

  • 영속성 전이란 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만드는 것을 말한다. 예를 들어 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하는 것을 의미한다.

CASCADE 없이 사용할 때 예제

Child 엔티티

package hellojpa;

import javax.persistence.*;

@Entity
public class Child {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;

    public Parent getParent() {
        return parent;
    }

    public void setParent(Parent parent) {
        this.parent = parent;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Parent 엔티티

package hellojpa;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

JpaMain

package hellojpa;

import javax.persistence.*;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {

            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();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • 생성한 객체마다 persist()를 해줘야 하는 불편함이 있다.

CASCADE 사용

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
  • 부모 연관관계에 cascade를 설정하면 parent만 persist()하면 자동으로 child도 영속화 된다.

부모와 자식의 라이프 사이클이 동일하고 단일 소유자 일때 (Parent를 제외한 다른 엔티티에서 Child를 소유하지 않는 경우) 사용하자.

고아 객체

  • 고아 객체란 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 의미한다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
  • Parent에서 고아객체를 삭제하도록 설정해보자.
package hellojpa;

import javax.persistence.*;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {

            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);
                        // em.remove(findParent); 부모를 제거하니 자식도 모두 제거 (CascadeType.REMOVE)와 동일하게 동작한다.
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • 부모에서 자식 객체 하나를 끊어보자.

image

  • 다음과 같이 자동으로 부모에게서 연관관계가 끊긴 자식 객체가 삭제된 것을 볼 수 있다.

고아 객체도 cascade와 같이 참조하는 곳이 하나일 때, 단일 소유자일 때 사용하자. 또한 @OneToOne, @OneToMany 만 사용 가능하다.

영속성 전이 + 고아 객체

  • 두 가지를 모두 사용하면 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다.
  • DDD의 Aggregate Root 개념을 구현할 때 유용하다.

예제

이전 포스팅부터 예제로 해온 프로젝트에 적용하자.

글로벌 페치 전략 설정

  • 모든 연관관계를 지연로딩(fetch = FetchType.LAZY)으로 변경하자. @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연 로딩으로 변경하자.

영속성 전이 설정

@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery;

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>()
  • Order 엔티티의 Delivery와 OrderItem을 영속성 전이 ALL 설정

본 포스팅은 인프런-김영한님의 '자바 ORM 표준 JPA 프로그래밍 - 기본편'을 참고하여 정리했습니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^