[JPA 기본] 프록시와 연관관계 관리 👨‍💻

홍정완·2022년 11월 6일
0

JPA

목록 보기
35/38
post-thumbnail

프록시



❓ 테이블을 조회해서 객체를 가져올 때 연관관계 객체는 안 가져오고 싶으면 어떻게 해야 할까


  • em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
Member member = em.getReference(Member.class, 1L);
System.out.println("member = " + member.getClass()); // HibernateProxy 객체



✅ 특징



  • 실제 클래스를 상속받아서 만들어진다.
  • 실제 클래스와 겉모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 구분할 필요가 없다. (이론적으로)
  • 프록시 객체는 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메서드 호출



  • 프록시는 처음 사용할 때 한 번만 초기화
  • 프록시 객체를 초기화할 때 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크 시 주의해야 함( == 비교 실패, 대신 instance of 사용)
m1.getClass() == m2.getClass() // false
m1 instanceof Member // true
m2 instanceof Member // true



  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = "+ m1.getClass()); // Member

Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " reference.getClass()); // Member

m1 == reference // true



  • 반대로 getReference()로 프록시 객체를 가지고 있으면 실제로 find()를 했을 때도 프록시 객체를 반환
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember = "+ refMember.getClass()); // Proxy

em.detach(refMember); // em.clear

refMember.getUsername(); // org.hibernate.LazyInitializationException



프록시 객체의 초기화


Member member = em.getRefernce(Member.class, "id1"); // (1)
member.getName(); // (2)



프록시 확인


  • 프록시 인스턴스의 초기화 여부 확인
    ersistenceUnitUtil.isLoaded(Object entity)

  • 프록시 클래스 확인 방법
    ntity.getClass().getName() 출력(..javasist.. or ibernateProxy…)

  • 프록시 강제 초기화
    rg.hibernate.Hibernate.initialize(entity);

  • 참고 : JPA 표준은 강제 초기화 없음
    강제 호출 : ember.getName()



즉시 로딩과 지연 로딩



✅ 지연 로딩


❓ Member를 조회할 때 Team(연관관계)도 함께 조회해야 할까

  • 단순히 member 정보만 사용하는 비즈니스 로직

  • 지연 로딩 LAZY을 사용해서 프록시로 조회
    fetch = FetchType.LAZY
@Entity
public class Member{
	...
	@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 사용
	@JoinColumn(name="TEAM_ID")
	private Team team;
	...
}
...
Member m = em.find(Member.class, member1.getId()); // Member 객체 반환
System.out.println("m = "+ m.getTeam().getClass()); // Team$HibernateProxy 객체 반환
m.getTeam().getName() // team을 실제로 사용하는 시점에서 db 조회 엔티티 반환
...

  • 연관관계에 있는 다른 엔티티를 사용하는 빈도수가 낮을 경우 지연 로딩을 사용해 불필요한 엔티티 조회를 막을 수 있다.



✅ 즉시 로딩


❓ Member와 Team을 같이 쓰는 빈도가 높을 경우에는 어떻게 해야 할까


  • 즉시 로딩 EAGER를 사용해서 함께 조회
    fetch = FetchType.EAGER

  • Member를 가져오는 시점에서 연관관계에 있는 Team까지 바로 가져오는 것을 즉시 로딩이라 한다.



✅ 프록시와 즉시 로딩 주의


  • 실무에서는 가급적 지연 로딩만 사용

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

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


@Entity
public class Member{
	...
	@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩 사용
	@JoinColumn(name="TEAM_ID")
	private Team team;
	...
}
...
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
// SQL : select * from Member
// SQL : select * from Team where TEAM_ID = xxx
...

위 JPQL을 그대로 쿼리로 번역하게 되면 Member를 가져오기 위한 쿼리 수행 이후 바로 Member 내부의 Team을 가져오기 위한 쿼리를 다시 수행하게 된다 → N+1 (1개의 쿼리를 날리면 +N 개의 쿼리가 추가 수행된다)


  • @ManyToOne, @OneToOne은 기본이 즉시 로딩으로 되어 있다. → 직접 전부 LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩



💡 N+1의 해결책


전부 지연 로딩으로 설정한 후, 가져와야 하는 엔티티에 한해서 fetch join을 사용해서 가져온다.

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



✅ 지연 로딩 - 실무


  • 모든 연관관계에 지연 로딩을 사용하자.
  • 실무에서 즉시 로딩을 사용하지 마라.
  • JPQL fetch join이나, 엔티티 그래프 기능을 사용해라.
  • 즉시 로딩은 내가 의도하지 않은 쿼리가 수행된다.



영속성 전이(CASCAD)와 고아 객체



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

    • ex) 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장



영속성 전이(CASCADE)를 이용한 엔티티 저장 방법


@Entity
public class Parent{
	...
	@OneToMany(mappedBy = "parent", cascade=CascadeType.ALL) // 영속성 전이 속성(CASCADE)사용
	private List<Child> childList = new ArrayList<>();

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

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

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

em.persist(parent);

CASCADE 종류

  • ALL : 모두 적용
  • PERSIST : 영속
  • REMOVE : 삭제
  • MERGE : 병합
  • REFRESH : REFRESH
  • DETACH : DETACH

❓ 영속성 전이(CASCADE)는 언제 써야 할까


  • 라이프 사이클이 동일할 때
  • 단일 소유자 관계일 때

하지만, 해당 엔티티(Child)가 특정 엔티티(Parent)에 종속되지 않고 여러 군데서 사용된다면 사용하지 않는 게 좋다.


고아 객체

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

    • orphanRemoval = true
@Entity
public class Parent{
	...
	@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);
	}
	...
}



고아 객체 - 주의


  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 하나일 때 사용해야 함.
  • 특정 엔티티가 개인 소유할 때 사용
  • @OneToOne, @OneToMany만 가능
  • Parent 객체를 지우게 되면 Parent가 소유하고 있는 ChildList에 속한 엔티티들이 전부 같이 삭제된다.



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


  • CascadeType.ALL + orphanRemoval=true
  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기 관리가 가능하다.
  • 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용
profile
습관이 전부다.

0개의 댓글