
우리가 유명한 맛집에 가서 밥을 먹을 때, 음식은 무조건 한 번에 주문해야 한다는 조건이 있다면 어떨까?
처음에는 다 먹을 수 있을 것 같아 주문할 수 있겠지만, 결국 마지막까지 입에 대지 않은 음식이 생길 수 있다.
이 경우 환불을 받을 수 없고 모든 음식의 비용을 우리가 부담해야 한다면 얼마나 억울할까?
우리가 앞으로 살펴볼 프록시 객체와 지연로딩은 위 예시와 다를 바가 없다.
엔티티가 음식이고 이를 우리가 가져오려고 하는데 연관된 모든 엔티티 객체가 한 번에 조회되는 경우
우리가 사용할 엔티티라면 상관 없지만 사용하지 않을 엔티티라면 그 비용이 쌓이고 쌓여 나중엔 서버 장애라는 무시무시한 결과를 발생시킬 것이다.
따라서, 스프링에서도 이를 방지하기 위해 프록시 객체와 함께 지연 로딩을 지원한다.
각각의 정의를 먼저 살펴보겠다.
프록시 객체란 쉽게 말해 가짜 객체를 말한다.

기존엔 em.find를 통해 찾고자 하는 엔티티를 가져왔다.
이 때 영속성 컨텍스트에 있으면 그 객체를 가져오고 없으면, DB에서 가져오는 과정을 거친다.
이 과정에서는 진짜 객체를 가져오게 된다.
Member Realmember = em.find(Member.class, 1L);
System.out.println("Realmember = " + Realmember.getClass());
//영속성 컨텍스트에 실제 객체가 존재하면 프록시 객체를 생성 안하므로 초기화
em.clear();
Member ProxyMember = em.getReference(Member.class, 1L);
System.out.println("ProxyMember = " + ProxyMember.getClass());
getReference가 가져오는 객체가 프록시 객체임을 확인할 수 있다.
Realmember = class jpabook.jpashop.domain.Member
ProxyMember = class jpabook.jpashop.domain.Member$HibernateProxy$uX2iWIeE
여기서 말하는 진짜 객체와 가짜 객체는 쉽게 이해할 수 있다.
진짜 객체는 모든 메서드와 속성을 실제로 구현한 프로그램에서 직접 사용되는 객체이고
가짜 객체는 실제 객체의 동작을 가로채는 객체
다음 그림처럼 생겼다.

이 그림을 보면 어느정도 감이 올 것이다.
"아 그러면 스프링에서는 실제 객체를 가져오지 않고 프록시 객체 생성을 통해서 연관된 객체를 가져오는 과정을 대체하는 거구나"라고 이해했으면 현재까지의 설명을 잘 이해한 것이다.
다음 그림을 보면 이런 과정을 더 깊게 이해할 수 있다.

이 그림을 쉽게 설명하자면, 스프링에서 프록시 객체 생성을 통해 진짜 객체를 대체하는 가짜 객체를 생성한 뒤 이것의 실제 메서드를 호출하기 전까지는 가만히 있다가
실제 객체 메서드를 사용하려고 클라이언트 호출이 발생하면 그때 실제 객체의 메서드를 대신 호출하는 방식으로 동작함을 알 수 있다.
이름만 들어도 대충 감이 올 것이다. 하지만, 정확한 정의를 살펴보자
지연로딩이란 필요할 때까지 객체나 데이터를 실제로 로드하지 않고 미루는 전략이다. 말로만 들어서는 어떤 장점이 있는 지 안 와닿을 수 있다.
프록시 객체 자체는 실제 객체가 아닌 가짜 객체이다.
때문에 초기화 되지 않은 상태로 클라이언트에게 전달되면, 해당 프록시가 내부에서 실제 데이터를 로드할 때 영속성 컨텍스트가 종료되어 있다.
따라서, LazyInitializationException 같은 문제가 발생할 수 있다.

쉽게 설명하자면
다음 그림에서 클라이언트가 실제 객체 메서드를 호출하기 위해서 초기화 요청을 보내면
프록시 객체는 영속성 컨텍스트의 참조를 가지고 있기 때문에, 만약 영속성 컨텍스트가 없는 상태면 에러가 발생한다.
프록시 객체가 영속성 컨텍스트를 참조로 가지는 이유에 대한 자세한 설명은 아래를 참고
프록시 객체가 영속성 컨텍스트를 참조로 가지는 이유
이 안에서 실제 객체를 찾거나 없으면 DB에서 찾아주는 과정을 영속성 컨텍스트가 해주기 때문이다.
그러면 준영속 상태의 프록시 객체가 DB를 직접 참조하면 되지 않나?
결론부터 말하자면, 안된다.
영속성 컨텍스트 자체가 DB 커넥션을 가지기 때문에 애초에 이게 없으면 DB에 접근할 수도 없다.
다음 그림 참고
여기서 중요한건 프록시 객체는 결국 초기화를 해야 사용할 수 있다는 것이다.
우리가 엔티티를 DB나 영속성 컨텍스트로부터 가져올 때 프록시 객체를 생성하고, 사용할 시점에 초기화 하고 클라이언트한테 넘겨주는 과정을 다 하기에는 너무 번거롭다.
따라서, 지연로딩 전략은 이를 모두 지원하고, 엔티티에 지연로딩 전략을 걸 수도 있기 때문에 실제 사용할 때 트랜잭션 안에서만 잘 초기화해서 클라이언트한테 넘겨주면 된다.
지연로딩은 다음과 같이 엔티티 안에서 연관 객체에 걸면 된다.
//Order 엔티티 내부 코드
...
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
여기서 @xxToOne 애노테이션이 적용된 엔티티와 @xxToMany가 적용된 엔티티는 서로 기본으로 적용되는 전략이 달라 헷갈리는데
딱 한 가지만 알면 된다.
"
@xxToOne전략은 모두fetch = LAZY적용"
@xxToMany 전략은 기본으로 지원하는게 fetch = LAZY이기 때문에 따로 선언할 필요 없이 기본으로 사용해도 지연 로딩이 적용된다.