자바 ORM 표준 JPA 프로그래밍 - 기본편 섹션 8. 프록시와 연관관계 관리
목차
프록시
즉시로딩과 지연로딩
지연로딩 활용
영속성 전이 : CASCADE
고아 객체
영속성 전이 + 고아객체, 생명주기
< 1. 프록시 >
예를 들어서 Member를 조회할 떄 Team도 DB에서 조회를 해야할까?
'엔티티'를 조회할때 연관된 엔티티들이 항상 사용되는것은 아니다. '연관관계'의 엔티티는 '비즈니스 로직'에 따라 사용될 때도 있지만 그렇지 않을때도 있다
try {
Member member = new Member();
member.setUsername("hello");
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 = " + findMember.getClass());
//Hibername가 만든 가짜 클래스 - 프록시 클래스
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.Username = " + findMember.getUsername());
//getUsername을 호출하는 시점에 JPA가 DB에 쿼리를 날린다.
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
private static void printMember(Member member) {
System.out.println("username = " + member.getUsername());
}
private static void printMemberAndTeam(Member member) {
String username = member.getUsername();
System.out.println("username = " + member.getUsername());
Team team = member.getTeam();
System.out.println("team = " + team.getName());
}
}
" '지연 로딩 기능'을 사용하려면 실제 엔티티 객체 대상에 '데이터베이스 조회'를 지연할 수 있는 '가짜 객체'가 필요한데 이것을 '프록시 객체'라 한다"
-> Hibernate는 '지연 로딩'을 지원하기 위해 '프록시'를 사용하는 방법을 지원한다.
프록시 - em.getReference() 라는 메서드 : DB에 '쿼리'가 날라가지 않는데 객체가 조회가 된다.
-> 프록시 객체를 가지고 온다음에 member.getName()을 호출하면 Member의 값이 처음에는 없다
-> MemberProxy 객체에 처음에는 target값이 존재하지 않는다. JPA가 '영속성 컨텍스트'를 통해서 '초기화'를 요청한다. 진짜 Member객체를 가져오게 한다
-> DB에 요청해서 조회해서 진짜 값을 연결해준다
-> '프록시 객체'의 Member target 변수에 '실제 객체'를 연결시켜준다
-> getName()을 하였을때 Member target을 통해서 Member.getName()이 반환된다
-> 프록시 객체에 target이 할당되고 나면, 더이상 '프록시 객체'의 초기화 동작은 없어도 된다.
-> "JPA 표준스펙에는 없다. Hibernate와 같은 라이브러리가 구현한다"
프록시의 특징
- 프록시 객체는 '처음 사용할 때' 한번만 초기화 -> 초기화한것을 계속 쓴다
- 프록시 객체를 초기화 할때, 프록시 객체가 실제 '엔티티'로 바뀌는것은 아님, 초기화되면 '프록시 객체'를 통해 '실제 엔티티'에 '접근' 가능
"프록시는 유지가 되고 내부의 target에만 값이 채워지는것이다"
//Member findMember = em.find(Member.class, member.getId());
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("before findMember = " + findMember.getClass());
System.out.println("findMember.username = " + findMember.getUsername());
System.out.println("after findMember = " + findMember.getClass()); //값이 바뀌지 않는다
tx.commit();
- (심화) 프록시 객체는 '원본 엔티티'를 상속받음, 따라서 '타입 체크'시 주의해야함( 타입비교할때 ==비교 실패, 대신 instance of 사용)
try {
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
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 == m2" + (m1.getClass() == m2.getClass())); //true
System.out.println("m1 == m2" + (m1.getClass() == m2.getClass())); //false
tx.commit();
}
private static void logic(Member m1, Member m2) {
System.out.println("m1 == m2 = " + (m1 instanceof Member));
System.out.println("m1 == m2 = " + (m2 instanceof Member));
}
- (심화) '영속성 컨텍스트'에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 '실제 엔티티' 반환
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass()); //Proxy
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getClass()); //Member
System.out.println("refMember == findMember = " + (refMember == findMember));
" JPA에서는 같은 인스턴스(같은 트랜잭션 레벨 = 같은 영속성컨텍스트 안에서) 안에서의 ==비교에 대해서 항상 '같다'라고 나온다"
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = " + m1.getClass()); // m1= class hellojpa.Member
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference.getClass() = " + reference.getClass()); //reference = class hellojpa.Member
System.out.println("a == a : " + (m1 == reference)); //true
tx.commit();
->" m1이 실제든 프록시든 상관없이 '== 비교'가 같은 영속성 컨텍스트안(같은 트랜잭션)에서 가져온 것이면 JPA는 항상 true를 반환해주어야만 한다" - JPA가 기본적으로 제공하는 메커니즘이다
- '초기화'는 영속성 컨텍스트의 도움을 받아야만 가능하다.영속성 컨텍스트의 도움을 받을 수 없는 '준영속 상태'일때, 프록시를 초기화 하면 문제가 발생한다.
-> 트랜잭션의 범위 밖에서 '프록시 객체'를 조회하려고 할때
(Hibernate는 org.hibernate.LazyInitializationException 예외를 터트림)
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass()); //Proxy
em.detach(refMember); //영속성 컨텍스트를 더이상 관리하지 않는다
refMember.getUsername();
프록시 확인
- 프록시 인스턴스의 '초기화 여부' 확인
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass()); //Proxy
refMember.getUsername(); //초기화
System.out.println("isLoaded= " + emf.getPersistenceUnitUtil().isLoaded(refMember)); //true
refMember.getUsername();
PersistenceUnitUtil.isLoaded(Object entity) - 프록시 클래스(인스턴스)의 초기화 여부를 직접 확인
entity.getClass().getName() 출력(..javasist..or HibernateProxy...) - 프록시 클래스 확인 방법
org.hibernate.Hibernate.initialize(entity); - 프록시 강제 초기화
참고 : JPA 표준은 '강제 초기화 없음
강제 호출 : member.getName()
-> '프록시'에 대한 메커니즘을 이해해야 '즉시 로딩'과 '지연 로딩'에 대해 이해할 수 있다.
< 2. 즉시 로딩과 지연 로딩 >
'Member 엔티티를 조회할 때 연관된 Team 엔티티도 함께 데이터베이스에서 조회하는것이 좋을까? 아니면 Member엔티티만 조회해두고 Team엔티티는 실제 사용하는 시점에 DB에서 조회하는것이 좋을까?
정답은 없다. 상황에따라 다를 수 있다.
즉시 로딩 : 엔티티를 조회할 떄 '연관된 엔티티'도 함께 조회한다
지연 로딩 : 연관된 엔티티를 '실제 사용할 때' 조회한다
지연 로딩 LAZY를 사용해서 '프록시'로 조회
@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) //지연 로딩 - 프록시객체 : Member클래스만 DB에서 조회한다
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) //읽기전용매핑, INSERT는 업데이트 X
private Team team;
try {
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
Member m = em.find(Member.class, member1.getId()); //쿼리가 멤버만 간다
System.out.println("m = " + m.getTeam().getClass()); // team은 Proxy로 가져온다
System.out.println("==============");
m.getTeam().getName(); //초기화
System.out.println("==============");
" '조회 대상'이 이미 영속성 컨텍스트 안에 있으면 Proxy객체를 사용할 이유가 없다. 따라서 Proxy가 아닌 '실제 객체'를 사용한다. 예를 들어 team1 엔티티가 영속성 컨텍스트에 이미 로딩되어 있으면 Proxy가 아닌 실제 team1 엔티티를 사용한다."
" 사용하는 시점에 주목 "
-> '쿼리'가 조회를 할때 Member와 Team을 조인을 하고 한방에 땡겨온다. 프록시가 필요없다.
-> 실제값을 출력해줌
프록시와 '즉시로딩' 주의
-> 가급적 '지연 로딩'만 사용 : 특히 실무에서
-> '즉시 로딩'을 적용하면 예상하지 못한 SQL이 발생
-> '즉시 로딩'은 JPQL에서 N+1을 일으킨다.
-> @ManyToOne, @OneToOne 은 기본이 '즉시 로딩' -> LAZY로 설정
-> @OneToMany, @ManyToMany는 기본이 '지연 로딩'
'지연 로딩' 활용 - 실무
-> 모든 연관관계에 '지연 로딩'을 사용해라
-> 실무에서 '즉시 로딩'을 사용하지 마라
-> 'JPQL fetch 조인'이나, '엔티티 그래프 기능'을 사용해라
-> '즉시로딩'은 상상하지 못한 '쿼리'가 나간다
< 3. 영속성 전이(CASCADE)와 고아 객체 >
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) //양방향 연관관계
private List<Child> childList = new ArrayList<>();
//Parent를 persist할때 컬렉션까지 다 persist해준다
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")
try {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
child1.setParent(parent); //연관관계 추가
child2.setParent(parent); //연관관계 추가
parent.addChild(child1);
parent.addChild(child2);
//부모저장, 연관된 자식들 저장
em.persist(parent);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
영속성 전이 : CASCADE - 주의
- '영속성 전이'는 '연관관계를 매핑하는것'과 아무 관련이 없음
- 엔티티를 영속화할때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.
CASCADE의 종류
ALL : 모두 적용
PERSIST : 영속 : 엔티티를 영속화 할때, 연관된 엔티티도 함께 유지
REMOVE : 삭제
MERGE : 병합 : 엔티티 상태를 병합할때, 연관된 엔티티도 모두 병함
REFRESH : 상위 엔티티를 '새로고침'할때, 연관된 엔티티를 모두 새로고침
< 4. 고아객체 >
정리
- JPA 구현체들은 '객체 그래프'를 마음껏 탐색할 수 있도록 지원하는데 이때 '프록시 기술'을 사용한다
- 객체를 조회할 때 연관된 객체를 즉시 로딩하는 방법을 '즉시 로딩'이라 하고, 연관된 객체를 지연해서 로딩하는 방법을 '지연 로딩'이라 한다
- 객체를 저장하거나 삭제할 때 '연관된 객체'도 함께 저장하거나 삭제할 수 있는데 이것을 '영속성 전이'라고 한다
- 부모 엔티티가 연관관계가 끊어진 '자식 엔티티'를 자동으로 삭제하려면 '고아 객체 제거 기능'을 사용하면 된다.