WEB/JPA

7. 자바 ORM 표준 JPA 프로그래밍 - 프록시와 연관관계 관리

프록시

Team에 속해있는 Member 를 조회할 시 Team까지 조회할 필요가 있을까?

비지니스 로직에서 필요하지 않을 때가 있는데 항상 Team을 함께 조회한다면 낭비가 발생된다.

이 낭비를 하지 않기 위해 프록시라는 갠며으로 해결한다.

 

 

- 실제 클래스를 상속 받아서 만들어지기 때문에 겉 모양이 같다

- 사용하는 입장에서는 진짜, 가짜 객체인지 구분하지 않고 사용하면 된다.

- 프록시 객체는 실제 객체의 참조를 보관 -> 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

 

em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회

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

      // 호출한 순간 쿼리 실행
      Member findMember = em.find(Member.class, member.getId());
      // 호출했을 때 실행 안함 
      Member findMember2 = em.getReference(Member.class, member.getId());
      // 이때 쿼리 실행 함 -> 실제 필요한 시점에 쿼리 실행
      System.out.println("findMember = " + findMember.getName());

 

 

프록시 객체의 초기화

Client에서 Member의 이름을 요청했을 때 영속성 컨텍스트에 제일 먼저 초기화를 요청한 후 영속성 컨텍스트가 실제 Entity를 만든것을 사용해서 초기화를 한다. 그 이후로 이름을 요청했을 때 taget으로 되어있는 Member를 조회하기 때문에 다시 DB조회를 하지 않아도 된다.

프록시의 특징

- 프록시 객체는 처음 사용할 때 한 번만 초기화

- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님 -> 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능

- 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 ( == 비교 대신 instance of 사용 )

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


      Member reference1 = em.getReference(Member.class, member1.getId());
      Member reference2 = em.getReference(Member.class, member2.getId());
           
      // true
      System.out.println("m1 == m2 : "+(m1.getClass() == m2.getClass()));
      // false
      System.out.println("reference1 == reference2 : " +(m1.getClass() == reference2.getClass()));
      // true      
      System.out.println("reference1 == reference2 : " +(m1 instanceof Member));
      // true
      System.out.println("reference1 == reference2 : " +(m2 instanceof Member));

파라미터로 프록시로 들어올지 실제로 들어올지 모르기 때문에 == 비교는 절때 사용하지 말것

타입을 비교할 때는 꼭 instanceof 를 사용해라!

 

- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getreference()를 호출해도 실제 엔티티가 반환한다.

	// m1.getClass -> 실제 클래스 반환
	Member m1 = em.find(Member.class, member1.getId());
	// m1.getClass -> 실제 클래스 반환
	Member reference = em.getReference(Member.class,member1.getId());
    

* 한 영속성 컨텍스트에서 호출했고, PK가 같다고 한다면 이미 영속성 컨텍스트에 있는 객체를 프록시로 반환해봤자 의미가 없기 때문에 실제 클래스를 반환한다.

* JPA는 하나의 영속성 컨텍스트에서 조회하는 같은 엔티티의 동일성을 보장한다.

 

반대로 아래와 같이 proxy 객체를 먼저 호출하고 실제 객체를 호출하게 된다면?

	// m1.getClass -> Proxy 객체 반환
	Member reference = em.getReference(Member.class,member1.getId());
	// m1.getClass -> Proxy 객체 반환
	Member m1 = em.find(Member.class, member1.getId());

 

- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제점

-> clear 후에 getName을 조회하면 not initialize proxy 에러가 뜬다.( 더이상 컨텍스트의 도움을 받지 못함)

      Member reference1 = em.getReference(Member.class, member1.getId());
      System.out.println(reference1.getClass());
      em.clear();
      System.out.println(reference1.getName());

 

 

프록시 확인

- 프록시 인스턴스의 초기화 여부 확인

	EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
	PersistenceUnitUtil persistenceUnitUtil = emf.getPersistenceUnitUtil();


	Member reference1 = em.getReference(Member.class, member1.getId());
	System.out.println(reference1.getName());
	// true
	System.out.println(persistenceUnitUtil.isLoaded(reference1));

- 프록시 클래스 확인 방법

entitiy.getClas().getName() 출력

- 프록시 강제 초기화

해당 entity를 강제 초기화 한다.

Hibernate.initalize(member);

 

즉시 로딩과 지연 로딩

* Member와 Team에서 Team은 잘 사용하지 않는다 했을 때

지연 로딩 LAZY을 사용해서 프록시로 조회

Member 와 ManyToOne 관계에 있는 Team을 조회할 때 프록시를 사용하겠다. 라고 정의 하는 것이다.(Member만 DB에서 조회)

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

- 아래의 그림 처럼 Member만 조회 했을 때 연관관계에 있는 Team은 조회하지 않고 Member 만을 조회한다. (Team는 가짜인 proxy객체가 있어서 조회되지 않음)

- 만약 Member에서 Team 관련 조회를 했을 때 그 시점에서 초기화가 된다( DB에서 직접 조회 )

 

* Member와 Team에서 자주 함께 사용한다면?

즉시 로딩 EAGER를 사용해서 함께 조회 ( Proxy를 사용하지 않는다 )

- 가능하면 JPA는 조인으로 한번에 조회한다.

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

 

* 주의할점

- 가급적 지연 로딩만 사용 한다. -> Member와 Team을 같이 조회하고 싶다면 JPQL fetch join이나 엔티티 그래프 기능으로 해결한다.

- 즉시 로딩을 적용하면 예상하지 못한 SQL 발생

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

- @ManyToOne, @OneToOne 은 기본이 즉시 로딩 --> LAZY로 설정

- @OneToMany, @ManyToMany 는 기본이 지연 로딩 

 

 

영속성 전이 : CASCADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때

-> 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장한다.

아래와 같이 연관관계 편의 메서드를 만든다.

	@Entity
	public class Parent{

	@OneToMany(mappedBy="parent",cascade = CascadeType.ALL)
	private List<Child> childList = new ArrayList<>();
    
	public void addChild(Child child){
		childList.add(child);
		child.setParent(this);
	}

여기서 cascade를 걸어주면 Parent를 persist할 때 Child도 같이 persist 해준다. 

주의 ) 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음!!!

그저 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화 시키는 편리함을 제공할 뿐이다.

 

CASCADE의 종류

- ALL - 모두 적용 * 해당 엔티티와 라이프사이클이 같을 때 사용한다 -> 해당 객체가 다른곳에서도 사용될 때 사용 X

- PERSIST - 영속 * 저장할 때만 쓸게 (삭제에 대해 조심할 때)

- REMOVE - 삭제

 등 다른 종류도 있지만 보통 ALL or PERSIST를 사용한다.

 

고아 객체

고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제

Cascade 와 orphanRemoval를걸어준다.

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

 

로직에서 Parent 의 Child List 에서 선택한 Child 요소를 remove 하면 DB에서도 삭제가 된다.

( 컬렉션에서 remove 하는데도 DB에서 같이 사라짐) 

 

* 주의 *

- 참조하는 곳이 하나일 때 사용해야함!

- 특정 엔티티가 개인 소유할 때 사용

- @OneToOne, @OneToMany만 가능하다.

- CascadeType.Remove처럼 작동한다.

 

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

둘다 사용하게 되면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.

예를 들어서 부모 엔티티가 Repository를 이용하여 삭제를 했을 때 자식은 Repository를 만들 필요 없이 지워지게 된다.