1. 프록시
❓Member를 조회할 때 Team도 함께 조회해야 할까?

<code />
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
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.getName());
}
private static void printMemberAndTeam(Member member) { //팀과 함께 출력
String userName = member.getName();
System.out.println("userName = " + userName);
Team team = member.getTeam();
System.out.println("team = " + team.getName());
}
1.1. ❓테이블을 조회해서 객체를 가져올 때 연관관계 객체는 안가져 오고 싶으면 어떻게 해야 할까?
1.1.1. em.find() VS em.getReference()
- em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
<java />
Member member = em.getReference(Member.class, 1L);
System.out.println("member = " + member.getClass()); // HibernateProxy 객체(가짜 클래스)
getReference() 메서드를 사용하면 진짜 객체가 아닌 하이버네이트 내부 로직으로 프록시 엔티티 객체 반환한다.
내부 구조는 틀은 같지만 내용이 비어있다. 내부에 target이 진짜 reference를 가리킨다.

1.2.
1.3. 프록시 특징
- 실제 클래스를 상속 받아 만들어지며, 실제 클래스와 겉 모양이 같다.
- 이론상으로 사용하는 입장에서는 진짜인지 프록시 객체인지 구분하지 않고 사용하면 된다. (이론상)
- 프록시 객체는 실제 객체의 참조(target)를 보관한다.
- 프록시 객체를 호출(
getName()
)하면 프록시 객체는 실제 객체의 메소드 호출한다. - 프록시는 처음 사용할 때 한 번만 초기화
<java />
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());
System.out.println("m1 : " + (m1 instanceof Member)); //true
System.out.println("m2 : " + (m2 instanceof Member)); //true
- 프록시 객체를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해 실제 엔티티에 접근 가능.
- 프록시 객체는 원본 엔티티를 상속받음. 따라서 타입 체크시 주의해야함(
==
: 비교 실패(type 다름). 대신instance of
사용)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면
em.getReference()
를 호출해도 실제 엔티티 반환
<java />
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = "+ m1.getClass());//Member
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " reference.getClass()); //Member
m1 == reference //true
이미 Member를 1차 캐시에도 올라와 있는데, 프록시를 반환할 필요가 없다.
- 반대로
getReference()
로 프록시객체를 가지고 있으면 실제로find()
를 했을때도 프록시 객체를 반환한다. - 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태(detach, clear, close 등)일 때, 프록시를 초기화하면 문제 발생
LazyInitializationException
예외가 발생한다.
- 한 트랜잭션 안에서 객체의 동일성을 보장 (프록시, 실제 객체 구별없이 동일성 보장 - 컬렉션 방식)
1.3.1. 프록시 객체 초기화 과정

getReference()
를 사용해서 조회하려는 값이 프록시가 갖고 있지 않은 값이면 영속성 컨텍스트에 초기화 요청을 하여 실제 객체에 값을 넣어줌.- 영속성 컨텍스트에서는 실제 db를 조회해서 가져온 다음 실제 Entity에 값을 넣어 생성한 다음 프록시 객체는 실제 엔티티를 연결해서 실제 엔티티를 반환한다.
- 그 이후에는 이미 초기화되어있는 프록시 객체이기에 해당 엔티티를 반환한다.
<code />
Member findMember = em.getReference(Member.class, member.getId()); // 프록시 객체 생성
System.out.println("findMember = " + findMember.getClass()); // ID는 갖고 있으므로 프록시에서 바로 반환
System.out.println("findMember.id = " + findMember.getId()); // ID는 영속성 컨텍스트에 초기화 요청을 통한 DB접근
System.out.println("findMember.userName() = " + findMember.getName());
- 프록시의 동작과정을 다음과 같이 확인해볼 수 있다.
getClass
를 찍어보면 프록시 클래스임을 알 수 있다.
1.4. 프록시 확인
- 프록시 인스턴스의 초기화 여부 확인 :
PersistenceUnitUtil.isLoaded(Object entity) → entityManagerFactory.getPersistenceUnitUtil().isLoaded(object)
- 프록시 클래스 확인 방법
entity.getClass().getname()
출력(..javasist.. or HibernateProxy...) - 프록시 강제 초기화
:org.hibernate.Hibernate.initialize(entity);
- 참고: JPA 표준은 강제 초기화 없음 강제 호출
:method.getName();
2. 즉시 로딩과 지연 로딩
2.1. 지연 로딩
❓Member를 조회할 때 Team도 함께 조회해야 할까?
: member 정보만 사용하는 비즈니스 로직에서
지연 로딩 LAZY을 사용해서 프록시로 조회 fetch = FetchType.LAZY
<java />
/*Member*/
@Entity
public class Member{
...
@ManyToOne(fetch = FetchType.LAZY) //지연로딩 사용
@JoinColumn(name="TEAM_ID")
private Team team;
...
}
...
Member m = em.find(Member.class, member1.getId()); //Member 객체 반환
System.out.println("m = "+ m.getTeam().getClass()); //Team$HibernateProxy객체 반환
m.getTeam().getName() // team을 실제로 사용하는 시점에서 초기화 (db조회)
...
2.2. 즉시 로딩
❓Member를 조회할 때 Team도 함께 조회해야 할까?
: Member와 Team을 같이 쓰는 빈도가 높을 경우
즉시 로딩 EAGER를 사용해서 함께 조회 fetch = FetchType.EAGER
<java />
/*Member*/
@Entity
public class Member{
...
@ManyToOne(fetch = FetchType.EAGER) //즉시로딩 사용
@JoinColumn(name="TEAM_ID")
private Team team;
...
}
...
Member m = em.find(Member.class, member1.getId()); //Member 객체 반환
System.out.println("m = "+ m.getTeam().getClass()); //Team 객체 반환
...
즉시 로딩 : Member를 조회하는 시점에서 Team도 같이 조회하는 것.(조인 사용)
2.3. 프록시와 즉시로딩 주의
- 가급적 지연 로딩만 사용 (특히 실무에서)
- 즉시 로딩을 적용하면 예상하지 못한 SQL 발생
: 하나의 엔티티에 연관된 엔티티가 많다면, find() 수행 시 수십, 수백개의 테이블을 한번에 불러와야 한다. - 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.
@ManyToOne
,@OneToOne
은 기본이 즉시 로딩으로 되어 있다. → 직접 전부 LAZY로 설정@OneToMany
,@ManyToMany
는 기본이 지연 로딩
2.4. 지연 로딩 활용 - 이론
- Member와 Team 은 자주 함께 사용 → 즉시 로딩
- Member와 Order는 가끔 사용 → 지연 로딩
- Order와 Product는 자주 함께 사용 → 즉시 로딩

2.5. 지연 로딩 활용 - 실무
- 모든 연관관계에 지연 로딩을 사용하자.
- 실무에서 즉시 로딩을 사용하지 마라.
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라.
- 즉시 로딩은 내가 의도하지 않은 쿼리가 수행된다.
3. 영속성 전이(CASCAD)와 고아 객체
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용.
ex) 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장.
3.1. 영속성 전이 : 저장
<java />
@Entity
public class Parent{
...
@OneToMany(mappedBy = "parent")
private List<Child> childList = new ArrayList<>();
public void addChild(Child child){
childList.add(child);
child.setParent(this);
}
...
}
@Entity
public class Child{
...
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
...
}
/*main*/
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);// persist 3번
이 코드에서는 persist를 3번이나 해야한다. 이는 효율적이지 못해 보인다.
persist 한 번으로 child 까지 같이 persist는 불가능 할까?
영속성 전이(CASCADE)를 이용한 엔티티 저장 방법
<java />
@Entity
public class Parent{
...
@OneToMany(mappedBy = "parent", cascade=CascadeType.ALL)//영속성 전이 속성(CASCADE)사용
private List<Child> childList = new ArrayList<>();
public void addChild(Child child){
childList.add(child);
child.setParent(this);
}
}
@Entity
public class Child{
...
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
...
}
/*main*/
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);// parent만 persist 해주면 child도 같이 persist된다.
CASCADE 종류
- ALL: 모두 적용(모든 곳에서 맞춰야 하면)
- PERSIST: 영속(저장할 때만 사용 할 것이면)
- REMOVE: 삭제
- MERGE: 병합
- REFRESH: REFRESH
- DETACH: DETACH
3.2. ❓그럼 영속성 전이(CASCADE)는 언제 써야 할까?
: 전이 될 대상이 한 군데에서만 사용된다면 써도 된다.
하지만, 해당 엔티티(Child)가 특정 엔티티(Parent)에 종속되지 않고 여러군데서 사용된다면
사용하지 않는게 좋다.
- 라이프 사이클이 동일할 때
- 단일 소유자 관계일 때
3.3. 고아 객체
고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제 : orphanRemoval = true
<java />
@Entity
public class Parent{
...
@OneToMany(mappedBy = "parent", cascade=CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child){
childList.add(child);
child.setParent(this);
}
...
}
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);// parent만 persist -> child도 같이 persist
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0); // orphanRemoval 동작
고아 객체 - 주의
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 참조하는 곳이 하나일 때 사용해야함.
- 특정 엔티티가 개인 소유할 때 사용
- @OneToOne, @OneToMany만 가능
- 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 마치 CascadeType.REMOVE처럼 동작한다.
- Parent객체를 지우게 되면 Parent가 소유하고있는 ChildList에 속한엔티티들이 전부 같이 삭제된다.
영속성 전이 + 고아 객체, 생명 주기 (CascadeType.ALL
+ orphanRemoval=true
)
- 스스로 생명주기를 관리하는 엔티티는
em.persist()
로 영속화,em.remove()
로 제거 - 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기 관리가 가능하다.
- 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용
Aggregate Root
: 연관깊은 도메인들을 각각이 아닌 하나의 집합으로 다루는 것. 즉 데이터 변경의 단위로 다루는 연관 객체의 묶음.
Aggregate에는 루트(root)와 경계(boundary)가 있는데, 경계는 Aggregate에 무엇이 포함되고 포함되지 않는지를 정의한다. 루트는 단 하나만 존재하며, Aggregate에 포함된 특정 엔티티를 가르킨다. 경계안의 객체는 서로 참조가 가능하지만, 경계밖의 객체는 해당 Aggregate의 구성요소 가운데 루트만 참조할 수 있다.
3.4. 실전 예제
3.4.1. 글로벌 페치 전략 설정
- 모든 연관관계를 지연로딩(
fetch = FetchType.LAZY
)으로 변경하자.
@ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연 로딩으로 변경하자.
3.4.2. 영속성 전이 설정
<java />
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>()
Order
엔티티의 Delivery와 OrderItem을 영속성 전이 ALL 설정
이 글은 김영한 님의 "자바 ORM 표준 JPA 프로그래밍 - 기본 편" 강의를 듣고 정리한 내용입니다.