본문 바로가기
Dev/JPA

[JPA] 프록시와 연관관계 관리

by dev_jsk 2021. 8. 27.
728x90
반응형

프록시

개요

'엔티티 조회 시 사용하지도 않는 정보도 같이 조회를 해야하는가?' 라는 의문이 생긴다.

 

예시

Member 조회 시 Team도 같이 조회해야 할까?

Member와 Team 모두 사용한다면 같이 조회되는 것이 효율적이지만 Member만 사용한다면 Team을 굳이 조회할 필요는 없는 것이다.

EntityManager.find() VS EntityManager.getReference()

  • EntityManager.find() : 데이터베이스를 통해 실제 엔티티 객체 조회
  • EntityManager.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회 (데이터베이스에 쿼리 실행되지 않는다.)

EntityManager.getReference()

예제

- Member.java

@Entity
public class Member {

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

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

    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;

    // getter, setter
}

- Team.java

@Entity
public class Team {
    
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();

    // getter, setter
}

- JpaMain.java

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.setUsername("hello");

            em.persist(member);

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

            // find() VS getReference()
            // EntityManager.find()
            Member findMember = em.find(Member.class, member.getId());
            // EntityManager.getReference()
            Member findMember = em.getReference(Member.class, member.getId());

            System.out.println("findMember = " + findMember.getClass());
            // 파라미터로 넘긴 값을 가져오기 때문에 select 쿼리 미실행
            System.out.println("findMember.id = " + findMember.getId());
            // 프록시를 초기화 하여 값이 없기 때문에 select 쿼리 실행
            System.out.println("findMember.username = " + findMember.getUsername());
            // 이미 초기화 되어있는 프록시에 값이 있기 때문에 추가적인 select 쿼리 미실행
            System.out.println("findMember.username = " + findMember.getUsername());
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        emf.close();
    }
}

동작 결과

(1) EntityManager.find() 결과

조회한 Member 클래스가 실제 객체이다.

(1) EntityManager.getReference() 결과

조회한 Member 클래스가 프록시 객체이다.

구조

  • 프록시 객체는 실제 클래스를 상속 받아서 만들기 때문에 실제 클래스와 겉모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 동일하게 사용하면 된다.
  • 프록시 객체는 실제 객체의 참조(target)를 보관한다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

프록시 객체는 실제 객체를 상속 / 프록시 객체는 실제 객체의 참조를 보관

프록시 객체의 초기화

Member memeber = EntityManager.getReference(Member.class, "id1");
member.getName();  // 강제 호출을 통한 프록시 초기화

프록시 객체 초기화 흐름

특징

  • 프록시 객체는 처음 사용할 때 한번만 초기화 된다. (여러번 사용해도 새로 초기화되지 않는다.)
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능하다.
  • 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크 시 주의해야 한다. ('==' 비교 시 실패, instanceof를 사용하여 체크해야 한다.)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 EntityManager.getReference()를 호출해도 프록시가 아닌 실제 엔티티 타입으로 반환한다. (프록시 사용 시 이점이 없고 JPA는 '==' 비교 시 true라고 보장해줘야 하기 때문이다.)
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다. (하이버네이트는 org.hibernate.LazyInitializationException 예외를 발생시킨다.)

특징별 예제

② 번

// JpaMain.java
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("before findMember = " + findMember.getClass());
// 강제 호출을 통한 프록시 객체 초기화
findMember.getUsername();
// 프록시 객체를 초기화 해도 프록시 객체
System.out.println("after findMember = " + findMember.getClass());

특징 2번 결과, 프록시 객체를 초기화해도 프록시 객체이다.

③ 번

// JpaMain.java
Member member1 = new Member();
member1.setUsername("hello");
Member member2 = new Member();
member2.setUsername("hello");

em.persist(member1);
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.class : " + m1.getClass());
System.out.println("m2.class : " + m2.getClass());
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass()));  // false
// Member 클래스 타입인지 체크
System.out.println("m1 instanceof Member : " + (m1 instanceof Member));
System.out.println("m2 instanceof Member : " + (m2 instanceof Member));

'==' 비교는 실제와 프록시 객체간의 비교기 때문에 false, instanceof 체크는 true

④ 번

(1) find() 호출 후 getReference() 호출 시

// JpaMain.java
Member member = new Member();
member.setUsername("hello");

em.persist(member);

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

// (1) find -> getReference
Member findMember = em.find(Member.class, member.getId());
Member refMember = em.getReference(Member.class, member.getId());

System.out.println("findMember : " + findMember.getClass());
// 영속성 컨텍스트에 이미 엔티티가 있기 때문에 프록시 미사용
System.out.println("refMember : " + refMember.getClass());
// refMember도 실제 엔티티 타입이므로 true
System.out.println("findMember == refMember : " + (findMember.getClass() == refMember.getClass()));

영속성 컨텍스트에 이미 실제 객체가 있기 때문에 해당 객체 그대로 사용

(2) getReference() 호출 후 find() 호출 시

// JpaMain.java
Member member = new Member();
member.setUsername("hello");

em.persist(member);

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

// (2) getReference -> find
Member refMember = em.getReference(Member.class, member.getId());
Member findMember = em.find(Member.class, member.getId());

System.out.println("refMember : " + refMember.getClass());
// 먼저 프록시 객체를 가져오면 find()도 프록시 객체를 반환
System.out.println("findMember : " + findMember.getClass());
System.out.println("refMember == findMember : " + (refMember.getClass() == findMember.getClass()));

먼저 프록시 객체를 가져오면 find()를 해도 프록시 객체 그대로 사용

(3) getReference() 2번 호출 시

// JpaMain.java
Member member = new Member();
member.setUsername("hello");

em.persist(member);

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

// (3) getReference 끼리 비교
Member refM1 = em.getReference(Member.class, member.getId());
Member refM2 = em.getReference(Member.class, member.getId());

System.out.println("refM1 : " + refM1.getClass());
System.out.println("refM2 : " + refM2.getClass());
// 두 객체 다 getReference()로 가져와도 == 비교를 true라고 보장해줘야 하기 때문에 같다.
System.out.println("refM1 == refM2 : " + (refM1.getClass() == refM2.getClass()));

'==' 비교 시 true를 보장해줘야 하기 때문에 같다.

⑤ 번

// JpaMain.java
Member member = new Member();
member.setUsername("hello");

em.persist(member);

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

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

// 영속성 컨텍스트 비우기(clear), 닫기(close), 제외하기(detach)
em.detach(refMember);  // em.clear() / em.close() 다 프록시 초기화 오류 발생

refMember.getUsername();  // 강제 호출을 통한 프록시 객체 초기화

준영속 상태 시 org.hibernate.LazeInitializationException 발생

유틸리티

  • 프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 : entity.getClass().getName() 출력
  • 프록시 강제 초기화 : org.hibernate.Hibernate.initialize(entity)
    ※ 참고로 JPA 표준은 강제 초기화가 없다. 강제 호출(ex. member.getUsername())을 하여 프록시를 초기화 한다.

유틸리티 예제

// JpaMain.java
Member member = new Member();
member.setUsername("hello");

em.persist(member);

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

Member refMember = em.getReference(Member.class, member.getId());
// 유틸리티 2번
System.out.println("refMember : " + refMember.getClass());

// 유틸리티 1번
System.out.println("before isLoaded : " + emf.getPersistenceUnitUtil().isLoaded(refMember));
// refMember.getUsername();  // 프록시 초기화(강제 호출)
// 유틸리티 3번
Hibernate.initialize(refMember);  // 강제 초기화
System.out.println("after isLoaded : " + emf.getPersistenceUnitUtil().isLoaded(refMember));

프록시 클래스 및 초기화 여부 확인, 강제 초기화 결과

즉시로딩과 지연로딩

개요

즉시로딩과 지연로딩도 프록시와 마찬가지로 '연관관계에 있는 객체를 사용여부에 따라 함께 조회해야 하는가?' 라는 의문을 갖는다.

지연로딩 (LAZY Loading)

지금 바로 조회하지 않고 연관관계의 객체를 사용하기 위한 방법

지연로딩 예시

구조

프록시 객체로 조회한다.

소스

- Member.java

@Entity
public class Member extends BaseEntity {

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

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

    @ManyToOne(fetch = FetchType.LAZY)  // 지연로딩 사용
    @JoinColumn
    private Team team;

    // getter, setter
}

- JpaMain.java

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

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

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

Member m = em.find(Member.class, member.getId());  // 지연로딩 설정 시 Member만 조회

System.out.println("m.team = " + m.getTeam().getClass());  // 지연로딩으로 인한 프록시 객체

System.out.println("before Proxy init");
m.getTeam().getName();  // 강제 호출로 프록시 객체 초기화
System.out.println("after Proxy init");

동작 결과

Team은 프록시 객체로 조회, 프록시 객체 내 값 접근 시 그때 DB에서 Team 조회

즉시로딩 (EAGER Loading)

연관관계의 있는 객체를 한번에 함께 조회하는 방법

즉시로딩 예시

구조

실제 객체로 조회한다.

소스

- Member.java

@Entity
public class Member extends BaseEntity {

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

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

    @ManyToOne(fetch = FetchType.EAGER)  // 즉시로딩 사용
    @JoinColumn
    private Team team;

    // getter, setter
}

- JpaMain.java

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

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

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

// 즉시로딩 설정 시 Member와 Team을 조인하여 함께 조회
Member m = em.find(Member.class, member.getId());
// 즉시로딩으로 인한 실제 객체
System.out.println("m.team = " + m.getTeam().getClass());
// 즉시로딩 시 실제 객체가 조회되기 때문에 실제 객체값 출력
System.out.println("m.team.name = " + m.getTeam().getName());

동작 결과

Member와 Team을 조인하여 조회하고 실제 Team 객체가 조회된다.

프록시와 즉시로딩 주의사항

  • 가급적 지연로딩만 사용한다. (특히 실무에서 중요!)
  • 즉시로딩을 적용하면 예상하지 못한 SQL이 발생한다.
  • 즉시로딩으로 설정된 연관관계를 가진 객체가 조인되어 조회되기 때문에 성능 저하가 발생한다.
  • 즉시로딩은 JPQL에서 N+1문제를 일으킨다. (N+1 : 결과가 +1 더 나온다.)
    해당 문제는 JPQL의 fetch join, 엔티티 그래프 어노테이션, 배치 사이즈를 이용해 해결 가능
  • @OneToMany, @ManyToMany기본이 지연로딩이지만 @ManyToOne, @OneToOne기본이 즉시로딩이라 지연로딩으로 바꿔서 설정해야 한다.

주의사항 예시

④ 번, JPQL에서 N+1 문제

// JpaMain.java
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.setUsername("userA");
member1.setTeam(teamA);
em.persist(member1);

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

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

// 즉시로딩 시 JPQL
// 쿼리는 Member만 조회하도록 작성했지만
// Member 객체 내 Team이 연관관계에 있어서 Team을 조회하는 쿼리도 실행된다.
// Member는 리스트로 한번에 조회하지만 Team은 각 맴버별 조회를 하기 때문에 2번의 쿼리가 실행된다.
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();

Team이 즉시로딩으로 설정되어있어 Team 정보 조회 쿼리가 Member 데이터 만큼 실행된다.

지연로딩 활용

  • 실무에서는 전부 지연로딩으로 사용하는 것이 필수적이다.
  • 이론적으로는 자주 함께 사용하는건 즉시로딩, 가끔 사용하는 것은 지연로딩을 사용하지만 실무에선 무조건 지연로딩을 사용하는 것이 좋다.

영속성 전이와 고아객체

영속성 전이 (Casecade)

특정 엔티티를 영속 상태로 만들때 연관된 엔티티도 함게 영속 상태로 만들고 싶을때 사용 (ex. 부모 엔티티 저장 시 자식 엔티티도 함께 저장)

영속성 전이 예시

구조

Parent와 Child가 1:N 관계

소스

- Parent.java

@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);
  }
  
  // getter, setter
}

- Child.java

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

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

  // getter, setter
}

- JpaMain.java

// JpaMain.java
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
// CASCADE 옵션을 지정했기 때문에 Parent 객체만 persist해도 Child 객체도 자동으로 persist 된다.
em.persist(parent);

동작 결과

Parent 저장 시 Child 객체도 자동으로 저장된다.

영속성 전이 종류

  • CascadeType.ALL : 모두 적용
  • CascadeType.PERSIST : 영속
  • CascadeType.REMOVE : 삭제
  • CascadeType.MERGE : 병합
  • CascadeType.REFRESH : 새로고침
  • CascadeType.DETACH : DETACH

영속성 전이 주의사항

  • 소유자가 1개일 경우(= 단일 소유자)에만 사용해야 한다. (다른 소유자도 같이 접근하면 운영하기 어렵다.)
  • 단일 엔티티에 종속적이여서 라이프사이클이 동일한 경우에만 사용해야 한다.
  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
  • 엔티티를 영속화 할 때 관련된 엔티티도 함께 영속화하는 편리함을 제공하는 것 뿐이다.

고아객체

  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이다. (orphanRemoval = true로 활성화)
  • 부모 엔티티 내 자식 엔티티를 컬렉션에서 제거할 경우 Delete SQL이 실행된다.

고아객체 예시

소스

- Parent.java

@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);
  }
  
  // getter, setter
}

- Child.java는 영속성 전이 예시와 동일

- JpaMain.java

// JpaMain.java
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());
// 1. 자식 객체만 제거
// 조회한 Parent 객체에서 0번째 Child 객체 제거, 따라서 0번째 Child 정보는 제거
findParent.getChildList().remove(0);

// 2. 부모 객체 제거
// 부모 객체 제거 시 자식 객체도 다 제거처리된다.
em.remove(parent);

동작 결과

(1) 자식 객체만 제거

자식 객체를 제거하는 Delete SQL이 실행된다.

(2) 부모 객체 제거

부모 객체 제거 시 자식 객체도 함께 삭제된다.

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

  • 스스로 생명주기를 관리하는 엔티티는 EntityManager.persist()로 영속화, EntityManager.remove()로 제거를 한다.
  • CascadeType.ALL + orphanRemoval = true 두 옵션을 모두 활성화 하면 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다.
  • 도메인 주도 설계(DDD)의 Aggregate Root개념을 구연할 때 유용하다.
728x90
반응형

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

[JPA] 값 타입  (0) 2021.09.01
[JPA] 실습 - 연관관계 관리  (0) 2021.08.27
[JPA] 실습 - 상속관계 매핑  (0) 2021.08.25
[JPA] 고급 매핑  (0) 2021.08.24
[JPA] 실습 - 다양한 연관관계 매핑  (0) 2021.08.23

댓글