자바 ORM 표준 JPA 프로그래밍 섹션8. 프록시와 연관관계 관리
프록시
아래와 같은 상황에서 Member을 조회할 때 Team도 매번 조회해야 할까? 답은 비즈니스적인 상황마다 다르다. 따라서 Member을 조회할 때마다 Team을 무조건 조회하는 것이 자원 낭비일 수 있다. 이런 문제를 JPA는 지연로딩과 프록시로 해결한다.
JPA에는 em.find() 말고도 em.getReference() 가 있다.
em.find()는 DB를 통해서 실제 엔티티 객체를 조회하는 것이고 em.getReference()는 DB 조뢰를 미루는 가짜(프록시) 엔티티 객체를 조회하는 것이다.
Member member = new Member();
member.setName("bella");
em.persist(member);
em.flush();
em.clear();
//Member findMember = em.find(Member.class, member.getId());
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.id="+findMember.getId());
System.out.println("findMember.username="+findMember.getName());
tx.commit();
위 코드를 통해 쿼리로 확인하면 em.find()는 Member와 Team을 join 하여 한 번에 데이터를 가지고 온다. 하지만 em.getReference()는 호출 시점에는 DB에 쿼리가 나가지 않지만 findMember가 실제 사용되는 시점에 쿼리가 나간다. 첫번째 System.out.println에서는 쿼리가 안나가지만 두번째에는 나간다는 이야기이다. 이때 findMember에는 프록시(가짜) 클래스가 저장되어 있다.
프록시 특징
실제 클래스를 상속 받아 만들어지는 것으로 실제 클래스와 겉 모양은 같다. 하지만 실제로는 안은 비어있다. 이론상으로는 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
프록시 객체는 실제 객체의 참조(target)을 보관한다. 프록시 객체를 호출하면 그때 프록시 객체가 실제 객체의 메소드를 호출한다. 예를 들어 Proxy의 getName()을 호출하면 실제 target의 getName()을 호출한다.
프록시 객체의 초기화
Member member = em.getReference(Member.class, “id1”);
member.getName();
처음 member은 proxy 객체를 가져온다. 이후 getName()을 호출하면 아래와 같은 과정이 진행된다.
getName()을 호출하면 처음 Member target에 값이 없기 때문에 JPA는 영속성 컨텍스트에 초기화를 요청한다. 그럼 영속성 컨텍스트가 DB를 조회하여 실제 Entity 객체를 생성하여 준다. 이후 이 진짜 객체를 MemberProxy의 target과 연결을 시켜준다. 따라서 getName()을 호출하면 target의 진짜 getName()을 통해 값을 가져오게 된다.
DB를 통해 진짜 값을 가지고 와서 진짜 엔티티를 만들어내는 것을 "초기화한다" 라고 한다. 즉 프록시에 값이 없을 때 진짜 값을 달라고 영속성 컨텍스트에 초기화를 요청하는 것이다. 한 번 초기화 하면 그후엔 target에 값이 있기 때문에 다시 DB를 조회할 일은 없다.
프록시의 특징
1. 프록시 객체는 처음 사용할 때 딱 한 번만 초기화 한다. 이후 그 내용을 계속 사용한다.
2. 프록시 객체를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다. 초기화 되면 프록시 객체를 통해 실제 엔티티에 접근 가능한 것이다. 내부 target에만 값이 채워지는 것이다.
3. 프록시 객체는 원본 엔티티를 상속 받는다. 따라서 타입 체크 시 주의가 필요하다. == 비교는 불가능하며 instance of 를 대신 사용해야 한다.
Member member1 = new Member();
member1.setName("bella1");
em.persist(member1);
Member member2 = new Member();
member2.setName("bella2");
em.persist(member2);
em.flush();
em.clear();
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.find(Member.class, member2.getId());
System.out.println("m1 == m2"+ (m1.getClass()==m2.getClass())); //true
Member m3 = em.getReference(Member.class, member2.getId());
System.out.println("m1 == m3"+ (m1.getClass()==m3.getClass())); //false
tx.commit();
위 방식이 아닌 아래처럼 비교해야 한다.
System.out.println("m1 == m3"+ (m1 instanceof Member)); //true
System.out.println("m1 == m3"+ (m3 instanceof Member)); //true
4. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1="+m1.getClass()); //Member
Member reference1 = em.getReference(Member.class, member1.getId());
System.out.println("reference1="+reference1.getClass()); //Member
System.out.println("a==a"+(m1==reference1)); //True
em.find()로 이미 Member가 영속성 컨텍스트에 있기 때문에 em.getReference()를 한다고 굳이 프록시 객체를 가져올 필요가 없다. 또한 마지막 print문에서 JPA는 하나의 인스턴스 안에서 무조건 == 비교에 true를 반환해야 하기 때문이다.
아래와 같은 경우에 proxy를 먼저 조회할 때에도 JPA가 == 비교에 true를 보장하기 위해 em.find()를 하더라도 proxy 객체를 반환한다.
Member reference2 = em.getReference(Member.class, member2.getId());
System.out.println("reference2="+reference2.getClass()); //Proxy
Member m2 = em.find(Member.class, member2.getId());
System.out.println("m2="+m2.getClass()); //Proxy
System.out.println("a==a"+(m2==reference2)); //Ture
5. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때 프록시를 초기화 하면 문제가 발생한다. Hibernate는 org.hibernate.LazyInitializationException 예외를 터트린다.
Member reference1 = em.getReference(Member.class, member1.getId());
System.out.println("reference1="+reference1.getClass()); //proxy
em.detach(reference1);
System.out.println("reference1's name="+reference1.getName());
org.hibernate.LazyInitializationException: could not initialize proxy - no Seesion
프록시 확인
JPA는 프록시를 확인해주는 utility 메소드가 있다.
- 프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity)
System.out.println("isLoaded="+emf.getPersistenceUnitUtil().isLoaded(reference1));
- 프록시 클래스 확인 방법 : entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)
System.out.println("reference1="+reference1.getClass());
- 프록시 강제 초기화 : org.hibernate.Hibernate.initialize(entity);
Hibernate.initialize(reference1);
즉시로딩과 지연로딩
JPA는 로딩 정책을 통해 객체를 즉시로딩과 지연로딩으로 가져올 수 있다.
지연로딩
지연로딩은 말그대로 진짜 객체를 로딩해오는 것을 미루는 것으로 진짜 객체 대신 프록시 객체를 불러온다.
FetchType.LAZY를 이용해 아래와 같이 Member의 Team을 프록시로 조회하도록 할 수 있다.
@Entity
public class Member extends BaseEntity{
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
...
}
이후 실제 team1을 사용하는 시점에 DB를 조회하여 초기화 한다.
즉시로딩
즉시로딩은 Member와 Team이 비즈니스 로직에 의해 항상 같이 쓰일 때 한 번에 조회할 수 있도록 해준다. 아래와 같이 FetchType.EAGER을 이용해 즉시로딩을 할 수 있다. 이때는 프록시가 생기지 않는다.
@Entity
public class Member extends BaseEntity{
...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
...
}
구체적인 쿼리를 살펴보면 JPA 구현체는 가능하면 조인을 사용하여 SQL 한 번에 함께 조회하겨 한다.
프록시와 즉시로딩 주의
가급적 지연로딩만 사용하는 것이 권장된다. 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생하기 때문인데 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. @ManyToOne과 @OneToOne은 기본이 즉시 로딩이기 때문에 LAZY 설정을 해주어야 한다. 반면에 @OneToMay와 @ManyToMany는 기본이 지연 로딩이다.
실무에서의 지연 로딩 활용
모든 연관관계에 지연 로딩을 사용하자. 실무에서는 즉시 로딩을 사용하면 안된다. 이후 JPQL fetch join, 엔티티 그래프 기능을 사용할 수도 있다.
영속성 전이: CASCADE
영속성 전이는 연관관계나 지연/즉시 로딩과는 관계 없는 내용이다. 부모를 저장할 때 연관된 자식도 자동으로 persist 호출을 하고 싶을 때 사용하는 것이다. 즉 영속성 전이란, 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용하는 것이다. 주로 단일 엔티티에 완전히 종속적일 때 사용한다. 두 엔티티의 라이프 사이클이 유사할 때와 단일 소유자 일 때 사용할 수 있다. 예를 들면 parent만 child를 소유할 때이다.
아래와 같이 CASCADE를 사용하면 parent만 persist 해도 child까지 persist 되는 것을 확인할 수 있다.
@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);
}
...
}
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name="parent_id")
private Parent parent;
...
}
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 {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
tx.commit();
} catch (Exception e){
tx.rollback();
} finally {
em.close();
}
emf.close();
}
CASCADE 종류
- ALL : 모두 적용
- PERSIST : 영속
- REMOVE : 삭제
- MERGE : 병합
- REFRESH : refresh
- DETACH : detach
고아 객체
고아 객체란 부모 엔티티와 연관관계가 끊어진 자식 엔티티이며 이를 자동으로 삭제하는 고아 객체를 제거하는 기능이 존재한다.
아래와 같이 orphanRemoval = true 를 설정하고 자식 엔티티를 컬렉션에서 제거하게 되면 DELETE 쿼리가 나간다.
@Entity
public class Parent {
...
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
...
}
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());
findParent.getChildList().remove(0);
즉 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다. 이 또한 참조하는 곳이 하나일 때만 사용해야 하고 특정 엔티티가 개인 소유할 때만 사용할 수 있다. @OneToOne, @OneToMany만 사용 가능하다.
개념적으로 부모를 제거하게 되면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면 부모를 제거할 때 자식도 함께 제거된다. 이는 CascadeType.REMOVE처럼 동작한다.
영속성 전이 + 고아 객체
영속성 전이와 고아 객체를 CascadeType.ALL + orphanRemoval=true 이렇게 섞어 쓸 수 있는데 이렇게 사용하게 되면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있다. 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화 하거나 em.remove()로 제거할 수 있지만 두 옵션을 다 활성화 하면 부모에 의해 자식의 생명 주기가 결정되므로 부모가 자식의 생명 주기를 관리할 수 있게 되는 것이다. 즉 자식의 DAO나 repository가 따로 필요 없게 된다. 이런 개념은 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.
'Spring > SpringBoot&JPA' 카테고리의 다른 글
[JPA] 객체지향 쿼리 언어 (0) | 2024.04.03 |
---|---|
[JPA] 값 타입 (기본값, 임베디드 타입, 불변객체, 비교, 컬렉션) (0) | 2024.04.02 |
[JPA] 상속관계 매핑 (0) | 2024.03.29 |
[JPA] 다양한 연관관계 (다대일, 일대다, 일대일, 다대다) (0) | 2024.03.29 |
[JPA] 연관관계 매핑 (단방향, 양방향, 연관관계의 주인) (0) | 2024.03.26 |