프록시(Proxy) 란?
객체를 DB에서 조회해서 가져올 때 연관 관계가 있는 객체들을 무조건 전부 가져온다면 해당 객체들을 사용하지 않을 때 효율적이지 않을 것이다. 그래서 해당 객체들을 실제로 가져오는 것 대신 중간에서 wrapper 역할을 하는 객체가 있는데 이 객체를 프록시라고 한다. 프록시는 진짜 엔티티 객체를 포함하고 있어서 원래의 엔티티 객체 사용 방법대로 사용할 수 있으며, 필요한 시점에 데이터를 가져올 수도 있다.
프록시 확인하기
@Test
@Transactional
public void proxyTest() {
User user = entityManager.getReference(User.class, 19); // 프록시 객체 획득
log.info("user = {}", user.getClass().getName());
// 프록시 객체가 비어있는지 확인
boolean loaded = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(user);
log.info("isLoaded = {}", loaded);
user.getName(); // 데이터 조회
// 프록시 객체가 비어있는지 확인
loaded = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(user);
log.info("isLoaded = {}", loaded);
}

프록시 안의 진짜 객체가 비어있는지 어떻게 알 수 있을까?
PersistenceUnitUtil을 이용하면 쉽게 확인할 수 있다. isLoaded() 메서드를 호출하여 확인할 수 있는 정보는 다음과 같다.
- 프록시 안의 엔티티 객체가 비어있으면 false 반환
- 프록시 안의 엔티티 객체가 있거나 프록시 객체가 아닌 엔티티 객체 자체일 경우면 true 반환
위의 테스트에서는 getReference()라는 메서드를 통해 프록시 객체를 가져오고 비어있는지 확인해 보았다.
그리고 데이터를 조회하는 작업을 수행한 후에 프록시 객체가 비어있는지 다시 확인해 보았다.
데이터 조회 후에는 select 쿼리를 수행하여서 프록시 객체 안의 엔티티 객체가 있음을 알 수 있다.
@Test
@Transactional
public void proxyTest2() {
User user = entityManager.find(User.class, 19); // 일반 엔티티 객체를 가져옴
log.info("user = {}", user.getClass().getName());
// 프록시 객체가 비어있는지 확인
boolean loaded = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(user);
log.info("isLoaded = {}", loaded);
}

일반 엔티티 객체인 경우를 살펴보자.
위의 결과를 보면 일반 엔티티 객체일 경우에도 true를 반환하는 것을 알 수 있다.
find() vs getReference()
@Test
public void proxyTest() {
User user = entityManager.find(User.class, 19);
log.info("user = {}", user.getClass().getName());
user = entityManager.getReference(User.class, 19);
log.info("user = {}", user.getClass().getName());
}

EntityManager의 find()는 트랜잭션 안이 아니어도 호출할 수 있다. getReference()는 해당 객체가 아닌 객체를 감싸고 있는 프록시 객체를 얻는 메서드이다. 프록시 객체 안의 진짜 객체는 아직 DB에서 가져오지 않은 상태이다. 진짜 객체는 프록시 객체를 이용해서 데이터를 조회하는 작업이 있을 때 가져온다.
@Test
public void proxyTest() {
User user = entityManager.find(User.class, 19);
log.info("user = {}", user.getClass().getName());
user = entityManager.find(User.class, 19);
log.info("user = {}", user.getClass().getName());
}

참고로 위처럼 트랜잭션 안이 아닌 곳에서 둘 다 find()를 하면 각각 개별적으로 실행된다.
@Test
@Transactional
public void proxyTest() {
User user = entityManager.find(User.class, 19);
log.info("user = {}", user.getClass().getName());
user = entityManager.getReference(User.class, 19);
log.info("user = {}", user.getClass().getName());
}

트랜잭션 안에서는 어떨까?
위의 결과를 보면 프록시 객체가 아닌 일반 엔티티 객체임을 알 수 있는데 이는 getReference() 메서드를 사용했다고 하더라도 이미 영속성 컨텍스트에 해당 객체가 존재하면 이미 존재하는 객체를 가져오기 때문이다.
@Test
@Transactional
public void proxyTest() {
User user = entityManager.getReference(User.class, 19);
log.info("user = {}", user.getClass().getName());
user = entityManager.find(User.class, 19);
log.info("user = {}", user.getClass().getName());
}

그럼 순서를 바꾸면 어떨까?
이번에는 프록시 객체만 생성됨을 알 수 있다. 그리고 영속성 컨텍스트의 캐시 안에 이미 해당 프록시 객체가 있어서 해당 객체를 가져오는 것을 알 수 있다.
@Test
@Transactional
public void proxyTest() {
User user = entityManager.find(User.class, 19);
log.info("user = {}", user.getClass().getName());
user = entityManager.find(User.class, 19);
log.info("user = {}", user.getClass().getName());
}

마지막으로 트랜잭션 안에서 find()를 두 번 호출하면 일반 엔티티 객체를 한 번만 조회해서 두 번째에는 영속성 컨텍스트에 있는 객체를 가져오는 것을 알 수 있다.
프록시 특징
- 프록시는 처음 한번만 초기화된다.
- 프록시 객체는 원본 엔티티를 상속받아 만들어진다.
- 영속성 컨텍스트에 이미 원하는 엔티티 객체가 있으면 프록시 객체가 만들어지지 않는다.
- 프록시 객체의 초기화는 영속성 컨텍스트에 의해서 이루어져야 한다.
프록시 & 식별자
@Test
@Transactional
public void proxyTest3() {
Post post = Post.builder().title("좋은 아침").body("반가워요").build(); // 게시글 생성
User user = entityManager.getReference(User.class, 19); // 회원 가져옴
post.setUser(user);
entityManager.persist(post); // 저장
}

프록시를 사용하면 원본 엔티티 객체가 비어있더라도 조회할 때 기본키를 사용하기 때문에 기본키에 대한 정보는 저장되어 있다. 그렇기 때문에 연관 관계가 있는 객체를 생성하고 저장할 때 해당 기본키만 이용하여 관계를 설정하기 때문에 따로 User에 대한 select 쿼리를 날리지 않은 것을 볼 수 있다.
@Test
@Transactional
public void proxyTest3() {
Post post = Post.builder().title("좋은 아침").body("반가워요").build(); // 게시글 생성
User user = entityManager.find(User.class, 19); // 회원 가져옴
post.setUser(user);
entityManager.persist(post); // 저장
}

만약 find()를 사용했었으면 위처럼 select 쿼리를 날려서 필요 없는 유저의 다른 정보들도 가져오게 될 것이다.
'공부 > JPA' 카테고리의 다른 글
| [JPA] LazyInitializationException 해결하기 (0) | 2024.02.28 |
|---|---|
| [JPA] OneToOne에서의 N + 1 문제 (0) | 2024.02.27 |
| [JPA] 다대다 연관 관계에서 복합키 사용하기 (0) | 2024.02.21 |
| [JPA] 연관 관계 중 인조키 vs 복합키 에 대한 고민 (0) | 2024.02.20 |
| [JPA] 연관 관계 매핑 확인하기 (0) | 2024.02.16 |