[자바 ORM 표준 JPA 프로그래밍 - 기본편] 08. 프록시와 연관관계 정리

Turtle·2024년 6월 19일
0
post-thumbnail

🙄프록시

  • ✔️프록시
    • em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
    • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
    • JPA에서 식별자로 엔티티 하나를 조회할 때 EntityManager.find()를 사용한다. 이 메서드는 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회한다. 이렇게 엔티티를 직접 조회하면 조회한 엔티티를 실제 사용하든 사용하지 않든 데이터베이스를 조회하게 된다.
    • 만약 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶다면 EntityManager.getReference()를 사용한다. 이 메서드를 호출할 때 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다. 대신에 데이터베이스 접근을 위임한 프록시 객체를 반환한다.
    • 프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉모양이 같다. 따라서 사용하는 입장에서 이것이 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
    • 프록시 객체는 실제 객체에 대한 참조를 보관한다.
    • 그리고 프록시 객체의 메서드를 호출하면 프록시 객체는 실제 객체의 메서드를 호출하게 된다.
    • 프록시 객체는 실제 사용될 때(Ex. member.getName()) 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라고 한다.
  • ✔️프록시 초기화 과정
    • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
    • 프록시 객체를 초기화한다고 해서 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
    • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
      • 원본 엔티티의 상속이므로 다형성 연산에 사용되는 instanceOf를 사용해서 비교
    • 영속성 컨텍스트에 찾는 엔티티가 이미 있다면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티가 반환된다.
    • 초기화는 영속성 컨텍스트의 도움을 받아야 한다. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 경우에서 프록시를 초기화하면 문제가 발생한다.
      • ❗문제 : LazyInitializationException
  • ✔️타입 체크
public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        try {
            Team team = new Team();
            team.setName("그냥 팀");
            em.persist(team);

            Member member1 = new Member();
            member1.setName("그냥 이름");
            member1.setTeam(team);
            member1.addTeamMember(team);
            em.persist(member1);

            Member member2 = new Member();
            member2.setName("그냥 이름2");
            member2.setTeam(team);
            member2.addTeamMember(team);
            em.persist(member2);

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

            Member findMember1 = em.find(Member.class, member1.getId());
            Member findMember2 = em.getReference(Member.class, member2.getId());
            logic(findMember1, findMember2);
            
            // findMember1.class = class hellojpa.domain.Member
			// findMember2.class = class hellojpa.domain.Member$HibernateProxy$AyFTuwby
			// false
			// findMember1 == findMember2 → true
			// findMember1 == findMember2 → true

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

    private static void logic(Member findMember1, Member findMember2) {
        System.out.println("findMember1.class = " + findMember1.getClass());
        System.out.println("findMember2.class = " + findMember2.getClass());
        System.out.println(findMember1.getClass() == findMember2.getClass());
        System.out.println("findMember1 == findMember2 → " + (findMember1 instanceof Member));
        System.out.println("findMember1 == findMember2 → " + (findMember2 instanceof Member));
    }
}
  • ✔️준영속 상태에서의 프록시 초기화 시 예외가 발생
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("ex");
        EntityManager em = emf.createEntityManager();
        EntityTransaction et = em.getTransaction();

        et.begin();

        try {
            Member member1 = new Member();
            member1.setName("hello1");
            em.persist(member1);

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

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

            Member m1 = em.getReference(Member.class, member1.getId());
            em.detach(m1);
            System.out.println(m1.getName());

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

실행 결과

org.hibernate.LazyInitializationException: could not initialize proxy [org.example.Member#1] - no Session
	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:165)
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:314)
	at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:44)
	at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:102)
	at org.example.Member$HibernateProxy$Nx2kSwiV.getName(Unknown Source)
	at org.example.Main.main(Main.java:27)
619, 2024 5:27:21 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PoolState stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/ex]

프록시 객체에서member1.getId() 메서드를 호출하면 실제 엔티티 객체를 영속성 컨텍스트에서 찾게 되나 flush() 메서드와 clear() 메서드에 의해 영속성 컨텍스트는 비워진 상황이므로 영속성 컨텍스트에게 데이터베이스에서 실제 엔티티 객체를 찾아달라고 요청을 하면 영속성 컨텍스트가 데이터베이스를 조회해서 찾게 된다.

하지만 영속성 컨텍스트에서 Member 객체를 준영속 상태로 전환하면 영속성 컨텍스트가 관리할 수 있는 범주를 벗어가게 되는데 이 때, getName() 메서드를 호출하면 초기화할 수 없다는 오류가 발생하게 되는 것이다.

  • ✔️프록시 확인
    • 프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object Entity)
    • 프록시 클래스 확인 : entity.getClass()
    • 프록시 강제 초기화 : org.hibernate.Hibernate.initalize(entity)
public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        try {
            Team team = new Team();
            team.setName("그냥 팀");
            em.persist(team);

            Member member1 = new Member();
            member1.setName("그냥 이름");
            member1.setTeam(team);
            member1.addTeamMember(team);
            em.persist(member1);

            Member member2 = new Member();
            member2.setName("그냥 이름2");
            member2.setTeam(team);
            member2.addTeamMember(team);
            em.persist(member2);

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

            Member findMember2 = em.getReference(Member.class, member2.getId());
            System.out.println("isLoaded : " + emf.getPersistenceUnitUtil().isLoaded(findMember2));

            System.out.println(findMember2.getClass());
            org.hibernate.Hibernate.initialize(findMember2);
            System.out.println("isLoaded : " + emf.getPersistenceUnitUtil().isLoaded(findMember2));

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

🙄즉시로딩과 지연로딩

@Entity
@Table(name = "MEMBERS")
public class Member extends BaseEntity {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String name;
    private String city;
    private String street;
    private String zipcode;

    @ManyToOne(fetch = FetchType.LAZY)	// 지연 로딩 옵션 적용
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}
@Entity
public class Team extends BaseEntity{
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
    @OneToMany
    @JoinColumn(name = "team")
    private List<Member> members = new ArrayList<>();
}

실행 결과

Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.city,
        m1_0.name,
        m1_0.street,
        m1_0.TEAM_ID,
        m1_0.zipcode 
    from
        MEMBERS m1_0 
    where
        m1_0.MEMBER_ID=?
  • ✔️지연 로딩 LAZY을 사용해서 프록시로 조회
    • 회원과 팀을 지연 로딩으로 설정했다.
    • EntityManager.find() 메서드를 호출하면 회원만 조회하고 팀은 조회하지 않는다.
    • 대신 조회한 회원의 Team 멤버변수에 프록시 객체를 넣어둔다.
    • 반환된 팀 객체를 프록시 객체다.
    • 이 프록시 객체는 실제 사용될 때(프록시 객체에서 실제 엔티티 객체의 메서드를 호출하는 시점)까지 데이터 로딩을 미룬다. 그래서 지연로딩이라고 하는 것이다.
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("shop");
        EntityManager em = emf.createEntityManager();
        EntityTransaction et = em.getTransaction();

        et.begin();

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

            Member member = new Member();
            member.setName("member");
            member.setTeam(team);
            em.persist(member);

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

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

            System.out.println("reference = " + reference.getTeam().getClass());

            System.out.println("=======================");
            reference.getTeam().getName();  // 초기화
            System.out.println("=======================");

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

실행 결과

// Member reference = em.find(Member.class, member.getId()); 실행 결과
// Member만 조회
Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.city,
        m1_0.createdBy,
        m1_0.createdDate,
        m1_0.modifiedBy,
        m1_0.modifiedDate,
        m1_0.name,
        m1_0.street,
        m1_0.TEAM_ID,
        m1_0.zipcode 
    from
        MEMBERS m1_0 
    where
        m1_0.MEMBER_ID=?
// LAZY 지연로딩 옵션을 적용하면 프록시로 조회하게 된다.
// LAZY 옵션 적용 후 클래스 타입 확인 → 프록시
// System.out.println("reference = " + reference.getTeam().getClass());
// reference = class org.example.domain.Team$HibernateProxy$WIG5TOzh
=======================
// 프록시 객체에서 실제 엔티티 객체의 메서드를 호출하는 순간 팀에 대해서 조회함
// reference.getTeam().getName();
Hibernate: 
    select
        t1_0.TEAM_ID,
        t1_0.createdBy,
        t1_0.createdDate,
        t1_0.modifiedBy,
        t1_0.modifiedDate,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.TEAM_ID=?
=======================
  • ✔️즉시 로딩 EAGER을 사용해서 함께 조회
@Entity
@Table(name = "MEMBERS")
public class Member extends BaseEntity {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String name;
    private String city;
    private String street;
    private String zipcode;

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

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

    public Team getTeam() {
        return team;
    }

    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("shop");
        EntityManager em = emf.createEntityManager();
        EntityTransaction et = em.getTransaction();

        et.begin();

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

            Member member = new Member();
            member.setName("member");
            member.setTeam(team);
            em.persist(member);

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

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

            System.out.println("reference = " + reference.getTeam().getClass());

            System.out.println("=======================");
            reference.getTeam().getName();  // 초기화
            System.out.println("=======================");

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

실행 결과

Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.city,
        m1_0.createdBy,
        m1_0.createdDate,
        m1_0.modifiedBy,
        m1_0.modifiedDate,
        m1_0.name,
        m1_0.street,
        t1_0.TEAM_ID,
        t1_0.createdBy,
        t1_0.createdDate,
        t1_0.modifiedBy,
        t1_0.modifiedDate,
        t1_0.name,
        m1_0.zipcode 
    from
        MEMBERS m1_0 
    left join
        Team t1_0 
            on t1_0.TEAM_ID=m1_0.TEAM_ID 
    where
        m1_0.MEMBER_ID=?
reference = class org.example.domain.Team
=======================
=======================
  • ✔️NULL 제약조건과 JPA 조인 전략
    • 즉시 로딩 실행 SQL에서 JPA가 내부 조인(inner join)이 아닌 외부 조인(left join)을 사용한 것을 잘 봐야한다.
    • 현재 회원 테이블에 있는 Team 외래 키는 NULL 값을 허용하고 있다.
    • 따라서 팀에 소속되지 않은(NULL) 회원이 있을 가능성이 있다.
    • 팀에 소속되지 않은 회원과 팀을 내부 조인하면 팀은 물론 회원도 조회할 수 없다.
      • 내부 조인의 특성상 일치하는 필드가 양쪽에 모두 있어야하며 같은 값을 가져야하므로
    • 외부 조인보다 내부 조인이 성능과 최적화 측면에서 더 유리하다.
    • 내부 조인을 사용하려면 외래 키가 NULL값을 허용하지 않는다는 제약 조건을 설정해야 한다.
    • @JoinColumn 어노테이션에 제약 조건으로 nullable = false를 사용하면 JPA는 외부 조인 대신에 내부 조인을 사용하게 된다
  • ✔️프록시와 즉시 로딩 주의
    • 가급적 지연 로딩만 사용(실무에서)
    • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
    • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
    • @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연 로딩으로 바꿔야 한다.
    • @OneToMany, @ManyToMany는 기본이 지연 로딩
  • ✔️지연 로딩 활용
    • 모든 연관관계에 지연 로딩을 사용해라
    • 실무에서 즉시 로딩을 사용하지 마라
    • JPQL fetch 조인이나 엔티티 그래프 기능을 사용해라
    • 즉시 로딩은 상상하지 못한 쿼리가 나간다.

🙄영속성 전이: CASCADE

  • ✔️영속성 전이: CASCADE
    • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용한다.
    • Ex. 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하고 싶을 때
@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;
    private String name;

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

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

	// Getter/Setter ...
}
@Entity
public class Child {
    @Id @GeneratedValue
    private Long id;
    private String name;

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

	// Getter/Setter ...
}
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("ex");
        EntityManager em = emf.createEntityManager();
        EntityTransaction et = em.getTransaction();

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

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

실행 결과

Hibernate: 
    /* insert for
        org.example.Parent */insert 
    into
        Parent (name, id) 
    values
        (?, ?)
Hibernate: 
    /* insert for
        org.example.Child */insert 
    into
        Child (name, parent_id, id) 
    values
        (?, ?, ?)
Hibernate: 
    /* insert for
        org.example.Child */insert 
    into
        Child (name, parent_id, id) 
    values
        (?, ?, ?)
  • ✔️영속성 전이: CASCADE 적용
    • Parent 클래스에서 cascade = CascadeType.ALL 옵션을 사용하면 Parent 클래스의 객체만 영속성 컨텍스트에 저장하면 연결된 Child 클래스의 객체들까지 같이 영속성 컨텍스트에 저장이 되는 것을 볼 수 있다.
    • 영속성 전이는 연관관계를 매핑하는 것과는 아무런 관련이 없다.
    • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함만을 제공할 뿐.
@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;
    private String name;
	
    ///////////////////////////////////////////////////////////
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
    private List<Child> children = new ArrayList<>();

	// 연관관계 편의 메서드
    public void addChild(Child child) {
        children.add(child);
        child.setParent(this);
    }
    ///////////////////////////////////////////////////////////

    // Getter/Setter ...
}
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("ex");
        EntityManager em = emf.createEntityManager();
        EntityTransaction et = em.getTransaction();

        et.begin();

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

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

			///////////////////////
            em.persist(parent); // 영속성 컨텍스트 저장
			///////////////////////

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

실행 결과

Hibernate: 
    /* insert for
        org.example.Parent */insert 
    into
        Parent (name, id) 
    values
        (?, ?)
Hibernate: 
    /* insert for
        org.example.Child */insert 
    into
        Child (name, parent_id, id) 
    values
        (?, ?, ?)
Hibernate: 
    /* insert for
        org.example.Child */insert 
    into
        Child (name, parent_id, id) 
    values
        (?, ?, ?)
  • ✔️고아 객체
    • 고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
    • orphanRemoval = true
    • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
    • 참조하는 곳이 하나일 때 사용해야함
    • 특정 엔티티가 개인 소유할 때 사용
    • @OneToOne, @OneToMany만 가능
@Entity
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> childList = new ArrayList<>();

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

	// Getter/Setter ...
}
  • ✔️영속성 전이 + 고아 객체, 생명주기
    • CascadeType.ALL + orphanRemovel=true
    • 스스로 생명주기를 관리하는 엔티티는 EntityManager.persist()로 영속화, EntityManager.remove()로 제거
    • 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음

🙄연관관계 관리

  • ✔️변경 사항
    • 모든 연관관계를 지연 로딩으로 변경해라
    • @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연 로딩으로 전부 변경해라
    • Order → Delivery를 영속성 전이 ALL로 설정해라
    • Order → OrderItem을 영속성 전이 ALL로 설정해라
@Entity
@Table(name = "ORDERS")
public class Order extends BaseEntity {
    @Id @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

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

    private LocalDateTime orderDate;
    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }
}

0개의 댓글