프록시 기초
프록시 : 실제 클래스를 상속 받아서 만들어진다. 이 때 해당 객체를 참조를 보관할 뿐이다.
em.find() vs em.getReference()
- em.find() : 데이터베이스를 통해 실제 엔티티 객체를 조회하는 것.
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 객체 조회
- DB에는 쿼리가 나가지 않지만 객체는 조회가 된다.
- 실제로 사용될 경우에만 select문으로 조회된다.
먼저 진짜 엔티티를 참조하는 Proxy 객체가 생성된다. 이후 영속성 컨텍스트에게 초기화 요청을 하면 DB에서 조회후 해당 Entity를 사용할 수 있게 되는 것이다.(Proxy가 진짜 엔티티가 되는 것이 아니라 DB에서 조회해온다는 것을 알아두자, 초기화는 처음 객체를 사용할 때 한번만 동작한다.)
프록시 타입 체크
그런데 proxy 타입을 비교할 일이 뭐가있을까?
성능 최적화 : 지연 로딩 전략을 사용 , 프록시 객체는 특정 메서드 호출 전에 실제 엔티티를 로드하지 않으므로, 성능을 최적화하기 위해 프록시인지 실제 엔티티인지를 구분할 경우가 있다. (emf.getPersistenceUnitUtil().isLoaded(entity))
데이터 일관성 유지 : 특정 비즈니스 로직에서 데이터가 변경되기 전에 항상 실제 엔티티가 필요하다면, 프록시 상태로 남아있는 것을 방지하기 위해 미리 실제 객체를 조회해야한다.
동일성 비교 : ****객체의 동일성을 비교할 때 , 프록시 객체와 실제 객체는 동일한 엔티티를 나타내지만, 프록시 객체는 실제 객체의 서브클래스로 생성된다는 것을 기억하자.
동일성 비교
//예제 1
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.find(Member.class, member1.getId());
//결과 : true - 같은 타입의 클래스 객체를 가져옴.
System.out.println("m1==m2: " + (m1.getClass() == m2.getClass()));
//예제 2
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());
//결과 : false
System.out.println("m1==m2: " + (m1.getClass() == m2.getClass()));
//결과 : true
System.out.println("m1==m2: " + (m1 instanceof Memdber);
getReference로 proxy를 가져왔기에 false가 출력된다. 하지만 타입을 비교한다는 의미는 proxy 객체 또한 같은 member 클래스를 상속하여 가져온 것이기 때문에 타입 체크시 true를 반환하는 것이 맞다. 따라서 실무에서타입을 비교할 경우 “instanceof”를 사용하자.
//예제 3
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member1.getId());
//결과 : true
System.out.println("m1==m2: " + (m1.getClass() == m2.getClass()));
m1과 m2는 모두 진짜 객체가 반환된다(proxy X). 이미 DB에서 조회를 마치면서 영속성 컨텍스트(1차 캐시)에서 값을 가져와 사용하기 때문에 따로 proxy 객체를 만들지 않는다.
// 예제 4
Member refmem = em.getReference(Member.class, member1.getId());
Member findmem = em.find(Member.class, member1.getId());
//결과 : true
System.out.println("m1==m2: " + (m1.getClass() == m2.getClass()));
이때 refmem와 findmem 진짜 객체가 아닌 proxy객체로 반환되게된다.
프록시 특징
프록시의 초기화는 영속성 컨텍스트에게 엔티티를 요청하면서 동작한다. 따라서 준영속 상태일 때는 프록시를 초기화 할 수 없다 . → could not initialize proxy exception 발생.
could not initialize proxy exception - stackoverflow
@ManyToMany(fetch = FetchType.EAGER)
은 기본이 Lazy이다.
질문자가 엔티티에@ManyToMany(fetch = FetchType.EAGER)
를 사용하거나 transactionManager를 사용해 authenticate
에 @Transcation를 추가하여 이를 해결할 수 있다. 이를 이용하면 즉시 데이터를 가져오게(EAGER)되며, @Transcation으로 authenticate
메서드가 실행되는 동안 DB에서 트랜잭션이 발생하고 DB에서 해당 데이터를 가져올 수 있게된다.
→ 하지만! @ManyToMany(fetch = FetchType.EAGER)
는 N+1 문제를 일으킬 수 있기 때문에 성능이 저하될 가능성이 매우 높다. 따라서 실무에서는 사용하지 말자.
즉시로딩과 지연로딩
- @ManyToOne, @OneToOne의 default : Eager Loading
- @OneToMany, @ManyToMany의 default : Lazy Loading
Lazy Loading은 proxy 객체를 불러온다. 실무에서는 (fetch = FetchType.Lazy
를 사용하자.
Eager Loading을 적용하면 예상하지 못한 SQL이 발생할 수 있다.
즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
- N+1 문제란?
- 한번만 조회하면 되는데 그 데이터가 참조하고 있는 다른 엔티티까지 모두 조회하게 되버리는 문제.
- 성능에 악영향.
@ManyToMany(fetch = FetchType.EAGER)
를 Member의 Team엔티티에 지정했다면…
List<Member> members = em.createQuery("select m from Member m" , Member.class)
위와 같은 쿼리가 동작할 경우, 작동되는 sql은 다음과 같다.
select * from Member;
select * from Team where TEAM_ID == xxx
멤버가 참조하는 다른 모든 엔티티의 값을 가져오게된다. 예시는 하나의 엔티티만 Eager로 적용했지만, 여러개라면… 지옥이다.
따라서 모든 컬럼에 fetch = FetchType.Lazy
를 적용하여 당장 필요없는 엔티티는 proxy로 가져오고, 필요 시 fetch join을 이용해서 해당 엔티티를 가져와 성능향상을 얻도록 하자.
영속성 전이(CASCADE)와 고아 객체
영속성 전이(CASCADE) : 엔티티에 값을 영속(삽입)할 경우 연관된 하위 엔티티에도 값을 삽입하는 경우
- @OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)
- ex) List<> 타입의 부모에 자식엔티티를 삽입하면, 자동으로 자식 엔티티도 만들어짐.
고아 객체(orphanRemoval) : 부모 엔티티가 사라지면, 자식엔티티도 같이 사라짐.
- @OneToMany(mappedBy = "parent", orphanRemoval = true)
@OneToMany(
mappedBy = "team",
orphanRemoval = true)
orphanRemoval = true 와 CascadeType.REMOVE의 차이
- CascadeType.REMOVE은 옵션의 경우에는 “부모 엔티티가 자식 엔티티와의 관계를 제거”해도 자식 엔티티는 삭제되지 않고 DB에 그대로 남아있다.(부모가 제거되면 자식이 DB에서 삭제된다.)
- orphanRemoval은 “부모 엔티티가 자식 엔티티와의 관계를 제거”하면 실제 DB에서도 삭제된다.