티스토리 뷰
프록시
는 무엇을 의미할까요? 이 질문에 앞서 프록시가 등장하게 된 배경에 대해 먼저 알아보도록 하겠습니다.
Proxy
등장 배경
객체는 객체 그래프로 연관된 객체들을 자유롭게 탐색할 수 있습니다. 하지만 데이터베이스 매핑을 하는 엔티티 객체에서는 자유도가 떨어집니다. 연관된 테이블의 데이터를 조회하기 위해서는 JOIN
을 사용하여 조회를 진행해야 하기 때문입니다.
자유로운 객체 그래프 탐색의 가능성으로 인해 연관된 모든 테이블을 조회하는 것은 비용이 따릅니다. 실제로 연관된 테이블을 사용하지 않는다면 쓸데없이 JOIN
을 하여 조회한 결과를 가져오기 때문입니다. 이 문제를 해결하기 위해 프록시가 등장하게 되었습니다.
연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라, 실제 사용하는 시점에 데이터베이스에서 조회할 수 있도록 해줍니다. 또한 자주 함께 사용하는 객체는 사용하는 시점이 아닌 해당 객체를 조회했을 때 바로 가져올 수 있도록 하는 방법도 존재합니다.
프록시란?
프록시의 등장 배경에 대해 알아보았습니다. JPA
에서 프록시는 실제 엔티티 객체 대신 데이터베이스 조회를 지연할 수 있는 가짜 객체를 의미합니다. 가짜 객체라고 해서 실제 엔티티의 동작을 수행하지 못하는 것은 아닙니다. 실제 엔티티 클래스를 상속받아 만들어지므로 실제 클래스와 겉모양이 같으므로 사용하는 입장에서는 진짜 객체인지 가짜 객체인지 구분하지 않고 사용하면 됩니다.
겉모양은 같아 사용하는 입장에서는 동일하게 사용하면 되지만 내부적으로는 다른 동작을 수행합니다. 프록시 객체는 실제 객체에 대한 참조를 가지고 있기에 프록시 객체의 메서드를 호출하면 프록시 객체는 참조를 통해 메서드 호출을 위임하고 실제 객체의 메서드를 호출하게 됩니다.
프록시 초기화
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setName("woochang");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println(findMember.getClass().getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
위의 코드는 데이터베이스 정보를 실제 엔티티 객체로 가져오는 방법입니다. 실행 결과로 package명.Member
를 출력하게 됩니다. 프록시 객체를 가져오고 싶을 땐 아래의 코드를 사용합니다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setName("woochang");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
System.out.println(findMember.getClass().getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
EntityManager
의 getReference
메서드를 통해 프록시 객체를 가져올 수 있습니다. 실행 결과로 package명.Member$HibernateProxy$misHOeS1
와 같은 형태로 출력하게 되는데 이를 통해 프록시 객체임을 확인할 수 있습니다. 하지만 아직 실제 엔티티 객체의 참조를 가지고 있지는 않습니다. 프록시 객체는 메서드 호출 등과 같이 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성 후 해당 객체를 참조로 가지게 됩니다. 이를 프록시 객체의 초기화라 합니다.
그림으로 이해하면 다음과 같습니다.
- getReference 호출을 통해 프록시 객체를 생성합니다. 해당 프록시 객체의 정보가 영속성 컨텍스트에 1차 캐시로 존재한다면 프록시 객체가 아닌 실제 객체를 반환합니다. 아직 사용이 존재하지 않기에 실제 엔티티 객체의 참조를 가지고 있지 않습니다.
- 실제 사용을 위해 메서드를 호출합니다.
- 메서드가 호출되면 영속성 컨텍스트에 초기화를 요청합니다. 실제 엔티티 객체의 참조를 가지기 위해 실제 엔티티 객체의 생성이 필요합니다.
- 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체의 정보를 얻습니다.
- 조회된 정보를 바탕으로 실제 Entity 객체를 생성합니다.
- 참조를 통해 실제 엔티티 객체의 메서드를 호출합니다.
- 메서드 호출의 결과를 반환합니다.
프록시 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화됩니다.
- 프록시 객체의 초기화는 실제 엔티티 객체로의 변경을 의미하는 것이 아니라 실제 엔티티 객체의 참조를 가지는 것으로 이해할 수 있습니다.
- 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시 주의해서 사용해야 합니다.
- 영속성 컨텍스트에 이미 존재한다면 데이터베이스 조회가 필요 없고 프록시 객체 사용 필요성이 없으므로 프록시 객체가 아닌 실제 엔티티 객체를 반환합니다.
- 동일성 보장을 위해 먼저 나온 객체가 프록시 객체일 때
find
를 사용하더라도 프록시 객체가 나올 수 있습니다.
- 동일성 보장을 위해 먼저 나온 객체가 프록시 객체일 때
- 초기화는 영속성 컨텍스트의 도움을 받아야 하기에 준영속 상태의 프록시를 초기화하면 문제가 발생합니다.
프록시 확인
해당 객체가 프록시인지 확인 하는 방법은 다음과 같습니다.
PersistenceUnitUtil.isLoaded(Object entity)
: 프록시 인스턴스 초기화 여부를 부울값으로 확인entity.getClass().getName()
: 프록시 클래스 이름 확인
즉시 로딩과 지연 로딩
앞서 말씀드린 것과 같이 프록시 객체를 사용하게 되면 자주 함께 사용하는 것은 바로 가져올 수도 있고, 사용할 때 가져올 수 있도록 설정하는 방법이 존재합니다. 바로 가져오는 방법이 즉시 로딩이고 사용할 때 가져오는 방법이 지연 로딩입니다.
즉시 로딩
엔티티를 조회할 때 연관관계에 있는 엔티티도 함께 조회하는 방법입니다. 즉시 로딩을 사용하기 위해서는 fetch
속성을 FetchType.EAGER
로 지정합니다. JPA
구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용합니다.
@Entity
public class Member {
// ...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
// ...
}
즉시 로딩 실행 SQL
에서 JPA
가 내부 조인(INNER JOIN)
이 아닌 외부 조인(LEFT OUTER JOIN)
을 사용하는 것을 확인할 수 있는데 이는 NULL
가능성 때문입니다. 내부 조인이 외부 조인보다 성능이 좋기에 최적화를 위해 내부 조인을 사용하는 것이 유리한데 이때는 외래키에 NOT NULL
제약 조건 설정이 필요합니다.
@Entity
public class Member {
// ...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID", nullable = false)
private Team team;
// ...
}
다음과 같이 외래키에 NULL
값을 허용하지 않는다고 JPA
에게 알려주는 경우 외부 조인 대신 내부 조인을 사용하게 됩니다.
지연 로딩
연관된 엔티티를 실제 사용할 때 조회하는 방법입니다. 지연 로딩을 사용하기 위해서는 fetch
속성을 FetchType.LAZY
로 지정합니다. 지연 로딩을 사용하게 되는 경우 실제 엔티티 객체 대신 앞에서 설명한 프록시 객체가 들어가게 됩니다.
@Entity
public class Member {
// ...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// ...
}
JPA 기본 페치 전략
@ManyToOne
,@OneToOne
: 즉시 로딩(FetchType.EAGER)@OneToMany
,@ManyToMany
: 지연 로딩(FetchType.LAZY)
로딩 전략 주의
되도록 지연 로딩만 사용하도록 권장하고 있습니다. 그 이유는 즉시 로딩을 사용하게 되면 예상치 못한 문제가 발생할 가능성이 존재하기 때문입니다. 예를 들면 JPQL
의 N+1
문제가 있습니다. select
로 Member
만 가져오는 SQL
을 작성했지만, 해당 SQL
의 개수만큼 필요하지도 않은 Team select SQL
가 발생하게 될 수도 있습니다.
그렇다고 무조건적인 지연 로딩을 사용하는 것이 아닌 상황에 알맞게 전략을 선택하도록 해야 합니다. 초기에는 예상치 못한 문제를 막기 위해 모든 것을 지연 로딩으로 설정한 뒤 실제 사용하는 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화하는 방법을 추천해 드립니다. 이런 간단한 최적화는 SQL
을 직접 사용하는 것이 아닌 JPA
를 사용함으로 얻게 되는 장점이라고도 할 수 있습니다.
'Spring > Spring Data' 카테고리의 다른 글
[JPA] 페치 조인(fetch join)이란? (4) | 2022.07.29 |
---|---|
[Error] 엔티티 인식 에러 해결 (0) | 2022.07.29 |
JPA 연관관계 매핑 정리 (0) | 2022.07.13 |
영속성 컨텍스트 (0) | 2022.07.05 |