엔티티 조회는 2가지 방식으로 할 수 있다.
- em.find() : DB를 통해서 실제 엔티티 객체 조회
- em.getReference() : DB 조회를 미루는 가짜(프록시) 엔티티 객체 조회(DB에 쿼리가 안나감)
프록시란?
- 실제 클래스를 상속 받아서 만들어져서 실제 클래스와 겉 모양이 같다. 따라서 이론상으로 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시는 가짜 객체로 실제 객체의 참조(target)을 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
프록시 객체의 초기화 과정

member에서 em.getReference()로 프록시 객체를 가지고 왔다.
- member.getName()을 호출하면 MemberProxy의 getName()을 보는데 Member target에 값이 없다면?
- 그러면 JPA가 영속성 컨텍스트에 진짜 Member 객체를 가져오라는 요청을 한다.
- 영속성 컨텍스트는 DB를 조회를 하고,
- 실제 Entity를 생성한다.
- 그리고 MemberProxy에 생성된 실제 Member 엔티티를 연결을 해준다.
그러면 결론적으로 Client가 getName()을 하면 target의 진짜 getName()을 통해서 Member가 있는 getName()이 반환이 된다.
프록시의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 위의 초기화 과정에서도 볼 수 있듯이, 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는게 아니라 초기화하면 프록시 객체를 통해서 실제 엔티티를 참조하는 것이다.
- 프록시 객체는 원본 엔티티를 상속받기 때문에 타입 체크할 때, == 비교 대신 instance of를 사용해야 한다.(비즈니스 로직을 사용할 때, 뭐가 넘어올지 확실하지 않기 때문) 또한 JPA에서 같은 인스턴스라는 == 비교에 대해서 같은 영속성 컨텍스트(트랜잭션 레벨) 안에서 조회하면 항상 같다고 나와야 한다.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있다면, em.getReference()를 호출해도 실제 엔티티가 반환된다.
- 준영속 상태(em.detach(member) 또는 em.close())일 때 프록시 초기화하면 문제 발생 → org.hibernate.LazyInitializationException
프록시 확인 방법
프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity)
Member refMember = em.getReference(Member.class, member1.getId()); // Proxy
System.out.println("refMember = " + refMember.getClass()); // refMember = class hellojpa.Member$HibernateProxy$LSGI8cGq
refMember.getUsername();
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); // isLoaded = true
프록시 클래스 확인 방법 : entity.getClass().getName() 출력 → 그냥 찍어봐야 됨
Member refMember = em.getReference(Member.class, member1.getId()); // Proxy
System.out.println("refMember = " + refMember.getClass()); // refMember = class hellojpa.Member$HibernateProxy$LSGI8cGq
프록시 강제 초기화
Member refMember = em.getReference(Member.class, member1.getId()); // Proxy
System.out.println("refMember = " + refMember.getClass());
Hibernate.initialize(refMember); // 강제 초기화
지연 로딩과 즉시 로딩
지연 로딩 : LAZY

프록시 객체로 실제 엔티티 객체를 참조하는 것으로 내부 메커니즘을 보면 member1을 로딩할 때, team 엔티티 인스턴스는 지연 로딩으로 되어있으므로 프록시로 가져온다.

- em.find()로 member 객체를 가지고 왔을 대, Member와 Team이 LAZY 지연 로딩으로 세팅이 되어있으면 가짜 프록시 객체를 놓는다.
- 그 다음에 이 member에서 Team을 가져와서 team의 어떤 값을 실제 사용하는 시점에 DB 초기화가 되면서 이때 쿼리가 나간다.
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
@JoinColumn(name = "team_id")
private Team team;
즉시 로딩 : EAGER

연관관계에 있는 객체까지 바로 조회하는 것으로 member1을 로딩을 할 때, 즉시로딩 EAGER로 세팅이 되어있으면 실제 team1 엔티티까지 조인해서 같이 가져온다.
프록시와 즉시 로딩 주의
- 실무에서는 즉시 로딩을 사용하면 안된다. 가급적 지연 로딩만 사용하자. → 그냥 무조건 지연 로딩만 사용 해라!!
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다. (테이블이 많이 엮여있을 때 어느 하나를 조회한다면 연관된 모든 테이블을 모두 조회하기 때문)
- JPQL에서 N+1 문제를 일으킨다. (JPQL에서 SQL로 번역이 되면서 쿼리가 한 번 나가고, FetchType.EAGER로 설정되어 있으면 이거에 대한 쿼리가 한 번 더 나간다. ;; 이건 확실하게 이해한지는 모르겠어요...) (N+1 에서 1은 JPQL의 최초 쿼리를 의미하고, N은 이로 인한 추가 쿼리 N개가 나간다고 함을 의미한다.)
- @ManyToOne, @OneToOne은 기본이 즉시 로딩(EAGER)이므로 반드시 지연 로딩(LAZY)으로 설정해야 한다.
- @OneToMany, @ManyToMany는 기본이 지연 로딩(LAZY)이다.
영속성 전이(CASCADE)와 고아 객체
영속성 전이: CASCADE
: 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다.
엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐, 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.

CASCADE를 사용 가능 전제 2가지
- Life Cycle이 같을 때
- 단일 소유자일 때(연관관계가 하나일 때)
CASCADE 종류
- ALL : 모두 적용
- PERSIST : 영속
- REMOVE : 삭제
- MERGE : 병합
- REFRESH
- DETACH
@Entity
@Getter @Setter
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) // parent를 persist() 할 때, ChildList에 있는 것들을 모두 persist() 한다.
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
}
@Entity
@Getter @Setter
public class Child {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
// 실행 코드
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); // child1과 child2가 모두 자동으로 생성
tx.commit();
고아 객체 - 제거
: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 것으로 orphanRemoval = true를 사용한다.
참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능으로 @OneToOne, @OneToMany만 가능하다.
단, 참조하는 곳이 하나이고, 특정 엔티티가 개인 소유할 때 사용할 수 있다.
참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.
@Entity
@Getter @Setter
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", orphanRemoval = true) // 부모가 제거되면 자식은 고아가 됨
private List<Child> childList = new ArrayList<>(); // 이 컬렉션에서 빠진 것은 삭제가 됨
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
}
// 실행 코드
/* ... 이전과 동일 ... */
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);
tx.commit();
영속성 전이 + 고아 객체, 생명 주기
CascadeType.ALL(영속성 전이) + orphanRemoval=true(고아 객체)를 함께 사용하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있다.
이 방식은 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.
@Entity
public class Parent {
...
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children = new ArrayList<Child>();
}
// 실행 코드
Parent parent = new Parent();
Child child1 = new Child();
parent.addChild(child); // 연관관계 매핑
// 부모만 등록,제거하면 자식은 자동으로 등록,제거됨
em.persist(parent);
em.remove(parent); // 전체 자식들 제거
parent.getChildList().remove(index:0); // 부모를 통해서 하나의 자식만 제거할 때
[출처]
https://www.inflearn.com/course/ORM-JPA-Basic -> 이 글은 김영한님의 JPA 강의 중 8장을 듣고 정리한 내용입니다.
'Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[JPA 기본편] 10. 객체지향 쿼리 언어1 - 기본 문법 (0) | 2024.06.23 |
---|---|
[JPA 기본편] 9. 값 타입 (0) | 2024.06.21 |
[JPA 기본편] 7. 고급 매핑 (0) | 2024.05.20 |
[JPA 기본편] 6. 다양한 연관관계 매핑 (0) | 2024.05.19 |
[JPA 기본편] 5. 연관관계 매핑 기초 (0) | 2024.05.18 |
엔티티 조회는 2가지 방식으로 할 수 있다.
- em.find() : DB를 통해서 실제 엔티티 객체 조회
- em.getReference() : DB 조회를 미루는 가짜(프록시) 엔티티 객체 조회(DB에 쿼리가 안나감)
프록시란?
- 실제 클래스를 상속 받아서 만들어져서 실제 클래스와 겉 모양이 같다. 따라서 이론상으로 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시는 가짜 객체로 실제 객체의 참조(target)을 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
프록시 객체의 초기화 과정

member에서 em.getReference()로 프록시 객체를 가지고 왔다.
- member.getName()을 호출하면 MemberProxy의 getName()을 보는데 Member target에 값이 없다면?
- 그러면 JPA가 영속성 컨텍스트에 진짜 Member 객체를 가져오라는 요청을 한다.
- 영속성 컨텍스트는 DB를 조회를 하고,
- 실제 Entity를 생성한다.
- 그리고 MemberProxy에 생성된 실제 Member 엔티티를 연결을 해준다.
그러면 결론적으로 Client가 getName()을 하면 target의 진짜 getName()을 통해서 Member가 있는 getName()이 반환이 된다.
프록시의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 위의 초기화 과정에서도 볼 수 있듯이, 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는게 아니라 초기화하면 프록시 객체를 통해서 실제 엔티티를 참조하는 것이다.
- 프록시 객체는 원본 엔티티를 상속받기 때문에 타입 체크할 때, == 비교 대신 instance of를 사용해야 한다.(비즈니스 로직을 사용할 때, 뭐가 넘어올지 확실하지 않기 때문) 또한 JPA에서 같은 인스턴스라는 == 비교에 대해서 같은 영속성 컨텍스트(트랜잭션 레벨) 안에서 조회하면 항상 같다고 나와야 한다.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있다면, em.getReference()를 호출해도 실제 엔티티가 반환된다.
- 준영속 상태(em.detach(member) 또는 em.close())일 때 프록시 초기화하면 문제 발생 → org.hibernate.LazyInitializationException
프록시 확인 방법
프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity)
Member refMember = em.getReference(Member.class, member1.getId()); // Proxy
System.out.println("refMember = " + refMember.getClass()); // refMember = class hellojpa.Member$HibernateProxy$LSGI8cGq
refMember.getUsername();
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); // isLoaded = true
프록시 클래스 확인 방법 : entity.getClass().getName() 출력 → 그냥 찍어봐야 됨
Member refMember = em.getReference(Member.class, member1.getId()); // Proxy
System.out.println("refMember = " + refMember.getClass()); // refMember = class hellojpa.Member$HibernateProxy$LSGI8cGq
프록시 강제 초기화
Member refMember = em.getReference(Member.class, member1.getId()); // Proxy
System.out.println("refMember = " + refMember.getClass());
Hibernate.initialize(refMember); // 강제 초기화
지연 로딩과 즉시 로딩
지연 로딩 : LAZY

프록시 객체로 실제 엔티티 객체를 참조하는 것으로 내부 메커니즘을 보면 member1을 로딩할 때, team 엔티티 인스턴스는 지연 로딩으로 되어있으므로 프록시로 가져온다.

- em.find()로 member 객체를 가지고 왔을 대, Member와 Team이 LAZY 지연 로딩으로 세팅이 되어있으면 가짜 프록시 객체를 놓는다.
- 그 다음에 이 member에서 Team을 가져와서 team의 어떤 값을 실제 사용하는 시점에 DB 초기화가 되면서 이때 쿼리가 나간다.
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
@JoinColumn(name = "team_id")
private Team team;
즉시 로딩 : EAGER

연관관계에 있는 객체까지 바로 조회하는 것으로 member1을 로딩을 할 때, 즉시로딩 EAGER로 세팅이 되어있으면 실제 team1 엔티티까지 조인해서 같이 가져온다.
프록시와 즉시 로딩 주의
- 실무에서는 즉시 로딩을 사용하면 안된다. 가급적 지연 로딩만 사용하자. → 그냥 무조건 지연 로딩만 사용 해라!!
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다. (테이블이 많이 엮여있을 때 어느 하나를 조회한다면 연관된 모든 테이블을 모두 조회하기 때문)
- JPQL에서 N+1 문제를 일으킨다. (JPQL에서 SQL로 번역이 되면서 쿼리가 한 번 나가고, FetchType.EAGER로 설정되어 있으면 이거에 대한 쿼리가 한 번 더 나간다. ;; 이건 확실하게 이해한지는 모르겠어요...) (N+1 에서 1은 JPQL의 최초 쿼리를 의미하고, N은 이로 인한 추가 쿼리 N개가 나간다고 함을 의미한다.)
- @ManyToOne, @OneToOne은 기본이 즉시 로딩(EAGER)이므로 반드시 지연 로딩(LAZY)으로 설정해야 한다.
- @OneToMany, @ManyToMany는 기본이 지연 로딩(LAZY)이다.
영속성 전이(CASCADE)와 고아 객체
영속성 전이: CASCADE
: 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다.
엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐, 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.

CASCADE를 사용 가능 전제 2가지
- Life Cycle이 같을 때
- 단일 소유자일 때(연관관계가 하나일 때)
CASCADE 종류
- ALL : 모두 적용
- PERSIST : 영속
- REMOVE : 삭제
- MERGE : 병합
- REFRESH
- DETACH
@Entity
@Getter @Setter
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) // parent를 persist() 할 때, ChildList에 있는 것들을 모두 persist() 한다.
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
}
@Entity
@Getter @Setter
public class Child {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
// 실행 코드
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); // child1과 child2가 모두 자동으로 생성
tx.commit();
고아 객체 - 제거
: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 것으로 orphanRemoval = true를 사용한다.
참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능으로 @OneToOne, @OneToMany만 가능하다.
단, 참조하는 곳이 하나이고, 특정 엔티티가 개인 소유할 때 사용할 수 있다.
참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.
@Entity
@Getter @Setter
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", orphanRemoval = true) // 부모가 제거되면 자식은 고아가 됨
private List<Child> childList = new ArrayList<>(); // 이 컬렉션에서 빠진 것은 삭제가 됨
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
}
// 실행 코드
/* ... 이전과 동일 ... */
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);
tx.commit();
영속성 전이 + 고아 객체, 생명 주기
CascadeType.ALL(영속성 전이) + orphanRemoval=true(고아 객체)를 함께 사용하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있다.
이 방식은 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.
@Entity
public class Parent {
...
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children = new ArrayList<Child>();
}
// 실행 코드
Parent parent = new Parent();
Child child1 = new Child();
parent.addChild(child); // 연관관계 매핑
// 부모만 등록,제거하면 자식은 자동으로 등록,제거됨
em.persist(parent);
em.remove(parent); // 전체 자식들 제거
parent.getChildList().remove(index:0); // 부모를 통해서 하나의 자식만 제거할 때
[출처]
https://www.inflearn.com/course/ORM-JPA-Basic -> 이 글은 김영한님의 JPA 강의 중 8장을 듣고 정리한 내용입니다.
'Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[JPA 기본편] 10. 객체지향 쿼리 언어1 - 기본 문법 (0) | 2024.06.23 |
---|---|
[JPA 기본편] 9. 값 타입 (0) | 2024.06.21 |
[JPA 기본편] 7. 고급 매핑 (0) | 2024.05.20 |
[JPA 기본편] 6. 다양한 연관관계 매핑 (0) | 2024.05.19 |
[JPA 기본편] 5. 연관관계 매핑 기초 (0) | 2024.05.18 |