예를 들어, Member를 조회할 때 Team도 함께 조회해야 할까?
메인
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 {
Member member = em.find(Member.class, 1L);
printMember(member);
//printMemberAndTeam(member);
tx.commit();
} catch (Exception e){
tx.rollback();
} finally {
em.close();
}
emf.close();
}
private static void printMember(Member member) {
System.out.println("member = " + member.getUsername());
}
private static void printMemberAndTeam(Member member) {
String username = member.getUsername();
System.out.println("username = " + username);
Team team = member.getTeam();
System.out.println("team = " + team.getName());
}
}
Member member = em.find(Member.class, 1L);에서 둘 다 가져오지만 경우에 따라 멤버 정보만 가져오고 싶거나 멤버와 팀을 같이 가져오고 싶다.프록시 기초
em.find() vs em.getReference()
em.find() : DB를 통해 실제 엔티티 객체 조회
try {
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
//System.out.println("findMember.id = " + findMember.getId());
//System.out.println("findMember.username = " + findMember.getUsername());
tx.commit();
}
아래와 같이 INSERT 쿼리 생성 후 SELECT 쿼리 생성

em.getReference() : DB 조회를 미루는 가짜(프록시) 객체 조회 (=DB에 쿼리가 날라가지 않고 조회가 된다.)

try {
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
//System.out.println("findMember.id = " + findMember.getId());
//System.out.println("findMember.username = " + findMember.getUsername());
tx.commit();
}
아래와 같이 INSERT 쿼리 생성 후 종료

만약 주석 처리 된 sout()을 주석 해제하고 실행하면?

id는 .getReference()의 파라미터로 쓰였기 때문에 DB에 접근하지 않고 값을 출력했지만, username은 DB에 SELECT 쿼리 실행findMember의 정체
try {
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
//System.out.println("findMember.id = " + findMember.getId());
//System.out.println("findMember.username = " + findMember.getUsername());
tx.commit();
}
print:
findMember = class hellojpa.Member$HibernateProxy$yBRBFOPb
프록시 특징 1)
실제 클래스를 상속 받아서 만들어진다. (=실제 클래스와 겉모습이 같다.)

사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다(이론상).
프록시 객체는 실제 객체의 참조(target)를 보관
프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출(위임)

💋프록시 특징 2)
프록시 객체는 처음 사용할 때 한 번만 초기화
Member member = em.getReference(Member.class, "pk");
member.getName(); //초기화
💋초기화 과정

.getName()(프록시 내 메소드)을 두 번째 이후로 호출할 때는 2~4 과정생략🧨프록시 객체 초기화 시, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니며, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능할 뿐이다. (target에만 값이 채워질 뿐!!)
🧨프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의(== 비교 X, instead of 사용)
🧨영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
try {
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member m1 = em.find(Member.class, member.getId());
System.out.println("m1 = " + m1.getClass());
/**
* JPA는 영속성 컨텍스트에 엔티티가 존재할 때,
* 1) 프록시를 반환해서 얻을 수 있는 성능 이점도 없고
* 2) == 비교할 때 true를 리턴해주기 위해 영속성 컨텍스트에 존재한 엔티티와 프록시 객체를 동일시한다.
* 바꾸어 말하면, 위의 em.find()와 아래의 em.getReference()의 위치를 바꾸면
* 미리 호출된 프록시 객체로 인해 em.find()도 프록시 객체가 호출된다. (==의 결과값은 true)
*/
Member reference = em.getReference(Member.class, member.getId());
System.out.println("reference = " + reference.getClass());
System.out.println("m1 == reference : " + (m1 == reference));
tx.commit();
print:
m1 = class hellojpa.Member
reference = class hellojpa.Member
m1 == reference : true
💥준영속 상태일 때, 프록시를 초기화하면 에러 발생 (org.hibernate.LazyInitializationException 예외)
try {
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("reference = " + refMember.getClass());
//영속성 컨텍스트 관리를 안하겠다! = 준영속상태
em.detach(refMember);
System.out.println("refMember = " + refMember.getUsername());
tx.commit();
} catch (Exception e) {
tx.rollback();
**System.out.println("e = " + e);**
}
print:
reference = class hellojpa.Member$HibernateProxy$AnC0DuF8
e = org.hibernate.LazyInitializationException: could not initialize proxy [hellojpa.Member#1] - no Session
프록시 유틸리티 메소드(프록시 확인법)
프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity)
try {
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("reference = " + refMember.getClass());
//초기화를 했기 때문에 결과는 true
refMember.getUsername();
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
tx.commit();
}
true! refMember.getUsername()을 주석 처리해 초기화를 하지 않으면 결과는 false프록시 클래스 확인 방법 : entity.getClass().getName()
프록시 강제 초기화 : org.hibernate.Hibernate.initialize(entity)
try {
...
Hibernate.initialize(refMember);
...
}
entity.getMethod())Member를 조회할 때, Team도 꼭 같이 조회해야하나? (같이:즉시로딩, 멤버만:지연로딩)
지연 로딩 LAZY를 이용해 프록시로 조회
@Entity
public class Member extends BaseEntity {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
...
}
try {
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("hello");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member m = em.find(Member.class, member.getId());
System.out.println("reference = " + m.getTeam().getClass());
/**
* 프록시 객체를 호출 및 초기화 했을때,,
*/
System.out.println("=======구분=======");
m.getTeam().getName();
System.out.println("=======구분=======");
tx.commit();
}
print:
Hibernate:
select
member0_.member_id as member_i1_1_0_,
member0_.createdBy as createdB2_1_0_,
member0_.createdDate as createdD3_1_0_,
member0_.lastModifiedBy as lastModi4_1_0_,
member0_.lastModifiedDate as lastModi5_1_0_,
member0_.team_id as team_id7_1_0_,
member0_.username as username6_1_0_
from
Member member0_
where
member0_.member_id=?
reference = class hellojpa.Team$HibernateProxy$EQocszM6
=======구분=======
Hibernate:
select
team0_.team_id as team_id1_4_0_,
team0_.createdBy as createdB2_4_0_,
team0_.createdDate as createdD3_4_0_,
team0_.lastModifiedBy as lastModi4_4_0_,
team0_.lastModifiedDate as lastModi5_4_0_,
team0_.name as name6_4_0_
from
Team team0_
where
team0_.team_id=?
=======구분=======
SELECT 쿼리 생성em.find()지만 m.getTeam()은 프록시 객체SELECT 쿼리 생성지연 로딩

지연 로딩(LAZY)을 사용해서 프록시로 조회

Member member = em.find(Member.class, pk);

Team team = member.getTeam();
team.getName(); // 실제 team을 사용하는 시점에 초기화(DB 조회(쿼리 생성))
Member와 Team을 자주 함께 사용한다면? ⇒ ❗즉시 로딩
즉시 로딩 EAGER를 사용해서 함께 조회
@Entity
public class Member extends BaseEntity {
...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
...
}
메인을 그대로 실행했을 때 결과
print:
Hibernate:
select
member0_.member_id as member_i1_1_0_,
member0_.createdBy as createdB2_1_0_,
member0_.createdDate as createdD3_1_0_,
member0_.lastModifiedBy as lastModi4_1_0_,
member0_.lastModifiedDate as lastModi5_1_0_,
member0_.team_id as team_id7_1_0_,
member0_.username as username6_1_0_,
team1_.team_id as team_id1_4_1_,
team1_.createdBy as createdB2_4_1_,
team1_.createdDate as createdD3_4_1_,
team1_.lastModifiedBy as lastModi4_4_1_,
team1_.lastModifiedDate as lastModi5_4_1_,
team1_.name as name6_4_1_
from
Member member0_
left outer join
Team team1_
on member0_.team_id=team1_.team_id
where
member0_.member_id=?
reference = class hellojpa.Team
=======구분=======
=======구분=======
JOIN을 이용한 SELECT 쿼리가 생성되어 DB에 Member와 Team에 대해 접근
프록시 객체가 아닌 진짜 객체가 나왔기 때문에 구분선의 초기화 값이 나오지 않는다.
즉시 로딩

Member 조회 시, Team도 항상 조회

💋프록시와 즉시 로딩 주의점
🧨가급적 지연 로딩만 사용(특히 실무에서)
즉시 로딩을 적용하면 예상하지 못한 SQL 발생
즉시 로딩은 JPQL에서 N+1 문제 야기
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
Hibernate:
/* select
m
from
Member m */ select
member0_.member_id as member_i1_1_,
member0_.createdBy as createdB2_1_,
member0_.createdDate as createdD3_1_,
member0_.lastModifiedBy as lastModi4_1_,
member0_.lastModifiedDate as lastModi5_1_,
member0_.team_id as team_id7_1_,
member0_.username as username6_1_
from
Member member0_
Hibernate:
select
team0_.team_id as team_id1_4_0_,
team0_.createdBy as createdB2_4_0_,
team0_.createdDate as createdD3_4_0_,
team0_.lastModifiedBy as lastModi4_4_0_,
team0_.lastModifiedDate as lastModi5_4_0_,
team0_.name as name6_4_0_
from
Team team0_
where
team0_.team_id=?
@XXXToOne은 기본이 즉시 로딩이기 때문에 ⇒ LAZY(지연 로딩)로 설정 변경
@XXXToMany는 기본이 지연 로딩
지연 로딩 활용 - 실무
지연, 즉시 로딩과 전혀 무관
영속성 전이(CASCADE) : 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때!
영속성 전이 : 저장
Parent
@Entity
public class Parent {
@Id
@GeneratedValue
@Column(name = "parent_id")
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> children = new ArrayList<>();
//편이 메소드
public void addChild(Child child){
this.children.add(child);
child.setParent(this);
}
//getter, setter
}
Child
@Entity
public class Child {
@Id
@GeneratedValue
@Column(name = "child_id")
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Parent parent;
//getter, setter
}
Main
try {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
//em.persist(child1);
//em.persist(child2);
tx.commit();
}
출력
print:
Hibernate:
/* insert hellojpa.Parent
*/ insert
into
Parent
(name, parent_id)
values
(?, ?)
Hibernate:
/* insert hellojpa.Child
*/ insert
into
Child
(name, parent_id, child_id)
values
(?, ?, ?)
Hibernate:
/* insert hellojpa.Child
*/ insert
into
Child
(name, parent_id, child_id)
values
(?, ?, ?)
em.persist(parent) ⇒ 연관관계의 child 모두 영속성 컨텍스트에 넣는다.CASCADE 주의
@OneToMany에서 mappedBy에 해당하는 컬럼이 One에 해당하는 클래스와 유일하게 연관관계를 가질 경우에만 쓰자)CASCADE 속성 종류
고아 객체
고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동 삭제
orphanRemoval = true
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childs = new ArrayList<>();
try {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
/**
* 영속성 전이(CASCADE)로 인해 parent만 영속성 컨텍스트에 저장해도
* parent에 연관관계를 맺은 child1, child2은 자동적으로 영속성 컨텍스트에 저장된다.
*/
em.persist(parent);
//em.persist(child1);
//em.persist(child2);
//remove(index)를 통해 index에 해당하는 데이터를 삭제
Parent parent1 = em.find(Parent.class, parent.getId());
parent1.getChilds().remove(0);
tx.commit();
}
DB 결과

Child를 2개 넣었지만 remove로 인해 한 개만 조회됐다.DELETE FROM CHILD WHERE ID=?
고아객체 주의
@OneToXXX만 가능CascadeType.REMOVE처럼 동작한다CASCADE + 고아 객체, 생명주기
CascadeType.ALL + orphanRemoval = true
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childs = new ArrayList<>();
Parent에 대해서 영속화하면 연관관계를 맺은 Child도 영속화하고, Parent를 지우면 Child에 대한 영속성 컨텍스트도 전부 지워진다.스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화 / em.remove()로 제거
But, 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명 주기도 관리 가능
DDD(도메인 주도 설계)의 Aggregate Root 개념을 구현할 때 유용
@ManyToOne,@OneToOne은 디폴트가 즉시 로딩이므로 지연 로딩으로 변경Order → Delivery 영속성 전이 ALL : 주문을 하면 배송도 같이 하게끔 생명주기를 관리Order → OrderItem 영속성 전이 ALL