728x90
1. 프록시
1.1. 프록시 기초
- em.find() vs em.getReference()
- em.find() : db를 통해서 실제 엔티티 객체 조회
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
1.2. 프록시 특징1
- 실제 class를 상속 받아서 만들어진다.
- 실제 클래스와 겉 모양이 같다.
- 실제 객체의 참조(target)을 보관
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메서드를 호출
1.3. 프록시 객체의 초기화
MemberChap8 member = new MemberChap8();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, "id");
findMember.getName();
- 초기화 설명
- flush()와 clear()로 영속성 context까지 싹 날린 상태
- getReference()를 하면 영속성 컨텍스트를 이용해서 Member 엔티티 (객체)를 생성을 하여 초기화 진행을 수행
- 현재는 영속성 컨텍스트를 아예 비운 상태이기 때문에 db로 접근하여 select 쿼리르 수행 후, 영속성 컨텍스트에 값 넣고, Member 엔티티도 생성
- 만약 flush(), clear() 안하면 영속성 컨텍스트에 값이 존재하기 때문에 쿼리를 날릴 필요없이 영속성 컨텍스트를 이용해서 바로 Member Entity 생성 - 이렇게 Member Entity 생성한 것을 이용해서 프록시 객체가 getName() 등의 해당 메서드를 수행한다.
- 참고로 em.find()로 수행 시, 바로 Member 엔티티를 직접 사용하는 것이고 em.getReference()를 이용하면 proxy라는 가짜를 앞에 두고 뒤에 숨겨서 만든 Member 엔티티를 이용해서 값을 가져오는 것이다.
- new Member()로 만들어서 사용하는데 왜 또 멤버 객체를 가져오는지 헷갈리지 말자
package hellojpa.chap8;
import hellojpa.Member;
import hellojpa.chap6.nByN.MemberNByN;
import hellojpa.chap7.MovieChap7;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.time.LocalDateTime;
public class JpaChap8 {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
tx.begin(); // tx 시작
try {
MemberChap8 member = new MemberChap8();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
// 해당코드 사용여부 상관없이 select에 join까지 쿼리 날라감
// MemberChap8 findMember = em.find(MemberChap8.class, member.getId());
// 당장 해당 코드 사용 안할시, 쿼리 안날림, sysout 주석풀면 query 날라감
MemberChap8 findMember = em.getReference(MemberChap8.class, member.getId());
System.out.println("findMember = " + findMember.getClass()); // class hellojpa.chap8.MemberChap8$HibernateProxy$xtE84MKo (프록시 객체)
// System.out.println("findMember.getId = " + findMember.getId()); // reference를 찾을 때 이미 id를 넣었기 때문에 쿼리 안날라감
// System.out.println("findMember.getUsername = " + findMember.getUsername()); // 이 때 db로 부터 값을 가져와서 findMember에 값을 채우고 출력함
// 프록시 객체는 값이 없으면 영속성 컨텍스트를 이용해서 실제 객체에 값을 넣고 다시 실제 객체를 통해서 가지고 오고, 실제 객체 값이 존재한 후부터는 영속성 컨텍스트에 접근하지 않고 바로 객체로 간다.
System.out.println("findMember.getUsername = " + findMember.getUsername()); // 영속성 컨텍스트를 통해서 DB 조회 - 쿼리 날라감
System.out.println("findMember.getUsername = " + findMember.getUsername()); // 실제 객체로 감 - 쿼리 안날라감
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
1.4. 프록시 특징2
- 처음 사용할 때 한 번만 초기화. 이후로는 영속성 컨텍스트를 거치지 않고 실제 Entity를 직접적으로 사용한다.
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 비뀌는 것이 아니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
- 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
- 프록시 객체랑 프록시 아닌 객체랑 type에 차이가 있기 때문 - 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 객체를 반환
- 반대로 proxy 객체로 처음에 호출되면 이후에 em.find()를 해도 프록시 객체를 반환
- 이유 : jpa는 동일 트렌젝션 안에서는 collection 처럼 동일 key로 value를 찾을 시 항상 동일한 value를 반환해야하기 때문에 type을 일치시킨다. - 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
- == 대신 instance of 사용하는 이유
package hellojpa.chap8;
public class JpaChap8InstanceOf {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
tx.begin(); // tx 시작
try {
// 프록시에 == 이 아닌 instance of 사용하는 이유
MemberChap8 member1 = new MemberChap8();
member1.setUsername("member1");
em.persist(member1);
MemberChap8 member2 = new MemberChap8();
member2.setUsername("member2");
em.persist(member2);
MemberChap8 member3 = new MemberChap8();
member3.setUsername("member3");
em.persist(member3);
em.flush();
em.clear();
MemberChap8 m1 = em.find(MemberChap8.class, member1.getId());
MemberChap8 m2 = em.find(MemberChap8.class, member2.getId());
MemberChap8 m3 = em.getReference(MemberChap8.class, member3.getId()); // proxy
System.out.println("m1.getClass() == m2.getClass() = " + (m1.getClass() == m2.getClass())); // true
System.out.println("(m1.getClass() == m3.getClass()) = " + (m1.getClass() == m3.getClass())); // false
logic(m1, m3); // 이럴 경우 프록시 객체가 언제 넘어올지 모르느낀 instance of 비교 해야함
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
private static void logic(MemberChap8 m1, MemberChap8 m3) {
System.out.println(m1 instanceof MemberChap8);
System.out.println(m3 instanceof MemberChap8);
}
}
- 동일 transaction 내에서 proxy, 엔티티 타입 일치
package hellojpa.chap8;
public class JpaChap8PC {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
tx.begin(); // tx 시작
try {
// 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
MemberChap8 member1 = new MemberChap8();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
MemberChap8 m1 = em.find(MemberChap8.class, member1.getId()); // 영속성 컨텍스트에서 찾는다.
System.out.println("m1 = " + m1.getClass()); // hellojpa.chap8.MemberChap8
MemberChap8 reference = em.getReference(MemberChap8.class, member1.getId());
System.out.println("reference = " + reference.getClass()); // hellojpa.chap8.MemberChap8 - 프록시가 안나온다!!
// 이유
// 1. 이미 1차 캐시에 담았는데 굳이 proxy를 사용할 이유가 없다.
// 2. jpa에서 동일 tx 안에서는 Collection 처럼 동일 key로 가지고 온 값은 항상 동일해야한다.
System.out.println("m1 == reference = " + (m1 == reference)); // 동일
em.clear();
// 반대로 proxy로 시작되면 find로도 proxy로 나온다.
MemberChap8 proxyMember = em.getReference(MemberChap8.class, member1.getId());
System.out.println("proxyMember.getClass() = " + proxyMember.getClass()); // class hellojpa.chap8.MemberChap8$HibernateProxy$C8QO027Z
MemberChap8 classMember = em.find(MemberChap8.class, member1.getId());
System.out.println("classMember.getClass() = " + classMember.getClass()); // class hellojpa.chap8.MemberChap8$HibernateProxy$C8QO027Z
System.out.println("(proxyMember == classMember) = " + (proxyMember == classMember)); //true
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
private static void logic(MemberChap8 m1, MemberChap8 m3) {
System.out.println(m1 instanceof MemberChap8);
System.out.println(m3 instanceof MemberChap8);
}
}
- 준영속 상태 + 프록시 메서드
- 프록시 인스턴스의 초기화 여부 확인 : emf.getPersistenceUnitUtil().isLoaded()
- 프록시 클래스 확인 방법 : entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)
- 프록시 강제 초기화 : org.hibernate.Hibernate.initialize(entity);
참고: JPA 표준은 강제 초기화 없음 강제 호출: member.getName()
package hellojpa.chap8;
public class JpaChap8Detach {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
tx.begin(); // tx 시작
try {
// 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
MemberChap8 member1 = new MemberChap8();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
// MemberChap8 refMember = em.find(MemberChap8.class, member1.getId()); // 영속성 컨텍스트에 의존하지 않는다.
MemberChap8 refMember = em.getReference(MemberChap8.class, member1.getId()); // 영속성 context에 의존한다.
System.out.println("refMember.getClass() = " + refMember.getClass()); // .getClass()는 proxy초기화 필요 없이 수행
System.out.println("proxy 초기화 확인 여부 메서드 = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
Hibernate.initialize(refMember); // 강제 초기화
System.out.println("proxy 초기화 확인 여부 메서드 = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
// System.out.println("refMember.getUsername() = " + refMember.getUsername()); // 이때는 프록시 초기화가 되기 때문에 이후부터는 객체랑만 소통해서 준영속 영향 안받음
// System.out.println("proxy 초기화 확인 여부 메서드 = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
// em.clear();
// em.detach(refMember);
em.close();
System.out.println("-> refMember.getUsername() = " + refMember.getUsername());
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
private static void logic(MemberChap8 m1, MemberChap8 m3) {
System.out.println(m1 instanceof MemberChap8);
System.out.println(m3 instanceof MemberChap8);
}
}
2. 즉시로딩과 지연 로딩
- @ManyToOne(fetch = FetchType.LAZY) : 지연로딩을하면 연관관계 참조 값들을 배제하고, 가지고와야할 경우 proxy로 가지고 온다.
package hellojpa.chap8;
@Entity
public class MemberChap8 {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
// @ManyToOne(fetch = FetchType.LAZY) // 지연로딩을하면 연관관계 참조 값들을 배제하고, 가지고와야할 경우 proxy로 가지고 온다.
@ManyToOne(fetch = FetchType.EAGER) // 조인해서 한번에 가지고 온다.
@JoinColumn(name = "TEAM_ID")
private TeamChap8 team;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
- 지연로딩을 할 시, proxy가 동작해서 필요한 시점에 연관관계가 존재하는 엔티티를 조인해서 값을 가지고 온다.
- team을 사용할 경우 쿼리를 한번 더 날려야하기 때문에 쿼리가 2번 나가게 된다.
- member와 team이 함께 사용되는 빈도가 높을 시, join해서 한 번에 가지고 오는 즉시로딩을 수행해야한다. (프록시를 사용하지 않는다.)
- fetch = FetchType.EAGER
2.1. 프록시와 즉시로딩 주의
- 가급적 지연 로딩만 사용 (특히 실무) - 해외 가이드에서도 권장
- 이유
- 즉시 로딩 적용시, 예상하지 못한 SQL이 발생
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. (어떻게 그런지 좀 더 찾아보기...)
- @ManyToOne, @OneToOne은 기본이 즉시 로딩 이므로 LAZY 설정 필요
- @OneToMany, @ManyToMany는 기본이 지연 로딩
2.2. 지연 로딩 활용
- 모델링 - 실습은 Member와 Team으로만 수행
- Member
package hellojpa.chap8;
@Entity
public class MemberChap8 {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
// @ManyToOne(fetch = FetchType.LAZY) // 지연로딩을하면 연관관계 참조 값들을 배제하고, 가지고와야할 경우 proxy로 가지고 온다.
@ManyToOne(fetch = FetchType.EAGER) // 조인해서 한번에 가지고 온다.
@JoinColumn(name = "TEAM_ID")
private TeamChap8 team;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
- Team
package hellojpa.chap8;
@Entity
public class TeamChap8 {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<MemberChap8> members = new ArrayList<>();
}
- 지연로딩
package hellojpa.chap8;
public class JpaChap8Lazy {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
tx.begin(); // tx 시작
try {
TeamChap8 team = new TeamChap8();
team.setName("teamA");
em.persist(team);
MemberChap8 member1 = new MemberChap8();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
MemberChap8 refMember = em.find(MemberChap8.class, member1.getId()); // lazy로 인해서 join 안된 상태로 member만 select
System.out.println("refMember.getClass() = " + refMember.getTeam().getClass()); // team은 proxy type으로 초기화 대기중
System.out.println("=== getTeam()을 사용시, proxy 초기화 시작 ===");
System.out.println(refMember.getTeam().getName()); // 프록시 초기화 하면서 쿼리가 날라감 * flush(), clear() 안하면 안날라감. 학습용으로 쿼리보려구 이렇게 설정
System.out.println("=== getTeam()을 사용시, proxy 초기화 끝 ===");
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
2.3. 지연 로딩 활용 - 실무
- 모든 연관관계에 지연 로딩을 사용
- 실무에서 즉시 로딩 사용 말 것
- JPQL fetch 조인, 엔티티 그래프 기능 사용
3. 영속성 전이 : casecade
- 즉시로딩, 지연로딩과 전혀 관계 없다.
- 부모 저장시 연관된 자식 호출하고 싶을 경우 사용 - persist나 remove 여러번 치기 싫을 경우 사용
- 하나의 부모가 자식들을 관리하는 경우 사용 - 게시판과 댓글, 첨부파일 경로
- 파일을 여러군데에서 관리할 시 사용하면 안됨
- 결론
- parent와 child의 life cycle이 동일 하고
- 단일 소유자 일 경우 사용
- 상황 : 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속상태로 만들고 싶을 경우
- ex) 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장
3.1. 주의
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련 없다.
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함 제공이 목적
- parent
package hellojpa.chap8;
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
//cascdeType,ALL : parent를 persist할 때, list 내부의 값들도 persist 해준다는 의미
//orphanRemoval true : 고아 객체, 부모의 childlist에서 관리를 안하게 될 경우, 자식 엔티티를 삭제 시켜버림
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
}
- child
package hellojpa.chap8;
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
}
- run
package hellojpa.chap8;
public class JpaChap8Cascade {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
tx.begin(); // tx 시작
try {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
System.out.println("=== persist시, sequence Object next value 가져옴 ===");
em.persist(parent);
System.out.println("=== persist시, sequence Object next value 가져옴 ===");
// em.persist(child1);
// em.persist(child2);
/* cascade
* 내가 지금 parent를 중심으로 개발을하고 있는데 child를 일일히 persist 하는게 불편...
* 그렇다고 em.persist(child)를 그냥 지우면 안다. child가 persist가 안되기 때문
* 이걸 해결해주는 것이 cascade
* */
// 고아객체 - 객체측에서 관계를 끊게 될 시, db 측에서 해당 row를 날려버림
System.out.println("=== flush, clear 시작 ===");
em.flush();
em.clear();
System.out.println("=== flush, clear 끝 ===");
System.out.println("===");
System.out.println("parent.getId() = " + parent.getId());
// sequence 전략으로 id를 가지고 올 때, 영속성 컨텍스트와 실제 객체에 값을 넣음 - flush, clear 영향 없음
System.out.println("===");
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
3.2. CASCADE 종류
- ALL: 모두 적용 (많이 사용)
- PERSIST: 영속 (많이 사용)
- REMOVE: 삭제
- MERGE: 병합
- REFRESH: REFRESH
- DETACH: DETACH
4. 고아 객체
- 객체의 입장에서 연관관계가 끊어질 경우 db에서 삭제가 된다.
- 고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
- orphanRemoval = true
- Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0);
// 자식 엔티티를 컬렉션에서 제거 - DELETE FROM CHILD WHERE ID=?
package hellojpa.chap8;
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
//cascdeType,ALL : parent를 persist할 때, list 내부의 값들도 persist 해준다는 의미
//orphanRemoval true : 고아 객체, 부모의 childlist에서 관리를 안하게 될 경우, 자식 엔티티를 삭제 시켜버림
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
}
4.1. 고아 객체 - 주의
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 주의
- 단일 소유시에만 사용
- 참조하는 곳이 하나일 때 사용해야함!
- 특정 엔티티가 개인 소유할 때 사용
- @OneToOne, @OneToMany만 가능 - cf> 영속성 정의는 논리적으로 들어가야 할 곳 아무곳이나 다 들어가진다.
- 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.
5. 영속성 전이 + 고아 객체, 생명주기
CasecadeType.All + orphanRemoval=true
- 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
- 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음
- 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용