프록시(Proxy)가 필요한 이유

즉시로딩은 엔티티를 조회할때 연관관계 맺고 있는 다른 엔티티도 같이 조회한다.
하지만 연관된 엔티티가 항상 필요하지 않을 수도 있기 때문에 불필요한 데이터 조회가 발생하게 된다. 이럴때는 지연로딩을 사용해야하는데, 이때 프록시를 사용하면 된다. 지연로딩은 실제 사용하는 시점에 데이터베이스를 조회하는데, 프록시는 지연로딩을 구현하는데 사용된다.

User 엔티티가 List<Post>를 가지고 있을 때, 
사용자가 User 정보만 필요하고 게시글은 필요하지 않은 경우, 
게시글은 프록시 객체로 대체되어 실제 데이터 접근이 필요할 때까지 로딩을 지연시킨다.

따라서 프록시의 주요 목적은 성능 최적화이다. 모든 관련 데이터를 즉시 로딩하는 것은 비효율적이기 때문에 프록시를 이용하여 실제 필요한 시점까지 데이터 로딩을 지연시킬 수 있어 리소스 사용을 최적화할 수 있다.

아래는 JPA를 사용하여 엔티티를 지연 로딩하는 예시 코드다.

@Entity
public class User {
    @Id
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    private Profile profile;

    ....
}

@Entity
public class Profile {
    @Id
    private Long id;

    private String details;

    ....
}

두 엔티티 클래스 UserProfile을 만들었는데, 위 코드를 보면 @OneToOne 관계에 fetch = FetchType.LAZY를 지정하였다. 그리고 Profile 엔티티에 접근하는 예시 코드를 아래와 같이 작성했다.

public class UserService {

    @PersistenceContext
    private EntityManager entityManager;

    public void accessUserProfile(Long userId) {
        User user = entityManager.find(User.class, userId);

        // 이 시점에서 Profile은 프록시 객체임
        Profile profile = user.getProfile();

        // Profile 엔티티의 데이터에 처음 접근하는 시점
        // 여기서 실제 데이터베이스 조회 발생함
        String details = profile.getDetails();
        
        ....
    }
}

이제 User 엔티티는 데이터베이스 조회할때 Profile 엔티티는 프록시 객체로 초기화 되며, getDetails() 메서드를 호출하는 순간 JPA는 실제 Profile 데이터베이스에서 로드한다.

프록시와 영속성 컨텍스트

프록시로 만들어진 엔티티에 대한 조회가 들어오면 영속성 컨텍스트는 실제 엔티티와 프록시 중에 어떤것을 반환할까 ? 영속성 컨텍스트의 동일성 보장의 특징에 따라 한 트랜잭션 내에서 일단 프록시 객체로 생성된 엔티티는 초기화 여부와 관계 없이 항상 동일한 프록시 객체를 반환하며, 최초의 실제 엔티티로 로드된 경우에는 이후에도 항상 실제 엔티티가 반환된다.

왜 이렇게 동작하는 이유로는 2가지가 있다.

  1. 일관성 유지: 영속성 컨텍스트는 엔티티의 동일성을 보장함으로써 애플리케이션 내에서 엔티티의 상태가 일관되게 유지되도록 한다. 이는 데이터 무결성과 일관성을 유지하는 데 중요한 역할을 한다

  2. 최적화: 프록시 객체와 실제 엔티티 사이의 전환을 최소화함으로써, JPA는 데이터베이스 접근을 최적화하고 성능을 향상시킨다.

예를 들어서, 한 트랜잭션 내에서 처음에 프록시 객체로 로드된 엔티티에 대한 후속 접근은 동일한 프록시 객체를 통해 이루어진다. 실제 데이터베이스에 대한 추가 접근을 방지하고 필요할 때만 데이터를 로드한다. 반대로도 실제 엔티티가 이미 로드된 경우에는 프록시 객체 대신 실제 엔티티가 반환되어 이미 로드된 데이터를 재사용한다.