프록시
Member 를 조회할 때 Team 도 함께 조회해야 할까?
Member 를 조회해서 username 만 사용할건데, Team 까지 같이 조회해오면 손해가 아닐까?
이를 최적화하기 위해 JPA 에선 프록시와 지연 로딩을 사용한다.
프록시 기초
em.find() 는 DB 를 통해서 실제 엔티티 객체를 조회하는 반면, em.getReference() 는 DB 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다.
실제로 em.find(Member.class, member.getId()) 를 통해 member 를 조회하면 SELECT 쿼리가 날아가지만, em.getReference(Member.class, member.getId()) 를 통해 member 를 조회하면 SELECT 쿼리가 날아가지 않는다.
한 편, Member findMember = em.getReference(Member.class, member.getId()) 로 조회한 후 findMember.getUsername() 을 호출할 경우 그제서야 SELECT 쿼리가 날아가는 것을 확인할 수 있다.
프록시 특징
프록시는 실제 클래스를 상속 받아서 만들어지기 때문에 실제 클래스와 겉 모습이 같다.
이론상 사용하는 입장에선 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면된다.
프록시 객체는 실게 객체의 참조(target)를 보관하기 때문에 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메서드를 호출한다.
프록시 객체의 초기화

Member member = em.getReference(Member.class, "id1");
member.getName();프록시의 특징
프록시 객체는 처음 사용할 때 한 번만 초기화한다.
프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능해지는 것이지, 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다.
프록시 객체는 원본 엔티티를 상속받기 때문에 타입 체크 시 주의해야 한다.
== 비교 대신에 instance of 를 사용해야 한다.
영속성 컨텍스트에 찾고자 하는 엔티티가 이미 em.find() 를 통해 존재한다면 em.getReference() 를 호출해도 실제 엔티티를 반환한다. 반대로, em.getReference() 로 먼저 엔티티를 조회하고 em.find() 로 같은 엔티티를 찾는다면, em.find() 로 반환된 엔티티는 프록시 객체임을 알 수 있다.
영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.
Hibernate 의 경우 LazyInitializationException 예외를 터트린다.
프록시 확인
emf.getPersistenceUnitUtil().isLoaded(refMember) 를 통해 프록시 인스턴스의 초기화 여부를 확인할 수 있다.
refMember.getClass() 를 통해 프록시 클래스를 확인할 수 있다.
Hibernate.initialize(refMember) 를 통해 프록시를 강제로 초기화 할 수 있다.
JPA 에는 강제 초기화가 없기 때문에 그냥 refMember.getName() 으로 강제 초기화 해도 된다.
즉시 로딩과 지연 로딩
지연 로딩 LAZY 를 사용해서 프록시로 조회
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}Member member = em.find(Member.class, 1L);
Team team = member.getTeam();
team.getName(); // 실제 team 을 사용하는 시점에 초기화Member 와 Team 을 자주 함께 사용한다면? EAGER 사용
Member member = em.find(Member.class, 1L); // 이 시점에 team 도 함께 조회
Team team = member.getTeam();
team.getName();프록시와 즉시로딩 주의
가급적이면 지연 로딩만 사용한다. 특히 실무에선 즉시 로딩은 거의 사용하지 않는다. 즉시 로딩을 적용하면 전혀 예상하지 못한 SQL 이 날아간다. 또한, 즉시 로딩은 JPQL 에서 N + 1 문제를 일으킨다. N + 1 문제를 해결하기 위해, 실무에서는 LAZY 로 다 바르고, A, B, C 를 한번에 조회해야 할 경우 fetch join 으로 한 방에 가져오는 방법을 주로 사용한다.
지연 로딩 활용
Member 와 Team 은 자주 함께 사용 → 즉시 로딩
Member 와 Order 는 가끔 사용 → 지연 로딩
Order 와 Product 는 자주 함께 사용 → 즉시 로딩

지연 로딩 활용 - 실무
- 모든 연관관계에 지연 로딩을 사용해라!
- 실무에서 즉시 로딩을 사용하지 마라!
- JPQL fetch join 이나, 엔티티 그래프 기능을 사용해라!
영속성 전이(CASCADE)와 고아 객체
영속성 전이 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<>();
}@Entity
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);
// cascade 적용 시 아래 코드를 적은 것과 같은 효과를 얻을 수 있다.
// em.persist(child1);
// em.persist(child2);주의
영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다. 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함만을 제공할 뿐이다.
CASCADE 의 종류
ALL모두 적용 - Life Cycle 을 맞춰야할 때PERSIST영속 - 같이 삭제되는 경우가 위험할 때
고아 객체
고아 객체 제거 - 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제하는 것이다.
orphanRemoval = true 설정을 주어 사용할 수 있다.
Parent parent = em.find(Parent.class, id);
parent.getChildren().remove(0);
// 연관관계가 끊어지면 DELETE 쿼리가 날아감고아 객체 - 주의
참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.
따라서, 참조하는 곳이 하나일 때 사용해야 하며 특정 엔티티가 개인 소유할 때 사용해야 한다.
@OneToOne @OneToMany 에서만 사용 가능하다.
orphanRemoval = true 을 사용하면 부모를 제거할 때 자식도 함께 제거된다.
영속성 전이 + 고아 객체, 생명주기
스스로 생명주기를 관리하는 엔티티는 em.persist() 로 영속화 혹은 em.remove() 로 제거할 수 있다.
CascadeType.ALL orphanRemovel=true 모두 활성화한다면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
실전 예제 5 - 연관관계 관리
글로벌 페치 전략 설정
모든 연관관계를 지연 로딩으로 설정한다.
영속성 전이 설정
Order → Delivery 를 영속성 전이 ALL 로 설정한다.
Order → OrderItem 을 영속성 전이 ALL 로 설정한다.