프록시란?
실제 엔티티 객체 대신 데이터베이스 조회를 지연할 수 있는 가짜 객체를 의미.
실제 객체를 사용하면 편한데 가짝 객체인 프록시를 쓰는 이유가 뭘까?
한 예시를 보고 알아보도록 하겠다.
Member를 조회할 때 Team도 함께 조회해야 될까?
JPA 관점에서 멤버와 팀을 동시에 조회하는 것은 멤버와 그들의 팀 정보를 함께 출력해야 할 때 유리하지만, 멤버 정보만 필요로 하는 상황에서는 자원의 낭비가 발생할 수 있다.
-> 이러한 문제는 프록시를 이용한 지연 로딩 방식으로 해결이 가능하다!
프록시 기초
- em.find() vs em.getReference()
- em.find() : 데이터베이스를 통해서 실제 엔티티 객체를 조회
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회(=DB에 쿼리가 안나가는데 객체가 조회가 된다)
프록시 특징
- 실제 클래스를 상속 받아서 만들어진다
- 실제 클래스와 겉 모양이 같다 (하이버네이트가 내부적으로 프록시 라이브러리를 사용해서 만들어낸다)
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다
- 프록시 객체는 실제 객체의 참조(target)를 보관한다
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
프록시 객체의 초기화
Member member = em.getReference(Member.class, "id1");
member.getName();
- Member 엔티티를 사용할 때(getName()을 호출) 프록시 객체에 대해 초기화 요청
- 영속성 컨텍스트에 실제 엔티티가 존재하지 않으면(Member target의 값이 없으면) 영속성 컨텍스트에 실제 엔티티에 초기화 요청(member객체)
- 영속성 컨텍스트가 DB를 조회해서 실제 Entity를 생성해서 준다
- 프록시 객체는 생성된 실제 엔티티의 객체를 필드에 보관하고, 메소드를 통해 결과를 반환
프록시의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화 된다
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다, 초기화 되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능해진다
- 프록시 객체는 원본 엔티티를 상속받는다, 따라서 타입 체크시 주의해야한다
(== 비교, 대신 instance of 사용 권장) - 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하는 문제가 발생한다
(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트린다)
Member member = em.getRefrence(Member.class, "id1"); // 프록시 객체
em.close(); // 영속성 컨텍스트 종료
member.getName(); // 준영속 상태 초기화 -> 실행 실패
준영속 상태일 때 예시) em.close()메소드로 영속성 컨텍스트를 종료했기 때문에 엔티티는 준영속 상태가 된다.
-> 이 때 member.getName()을 호출하면 예외 발생!
즉시 로딩과 지연 로딩
프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용한다.
여기서 지연 로딩이란, 엔티티를 DB에서 바로 조회하는 것이 아니라 실제로 사용될 때 해당 엔티티를 조회하는 방식을 의미한다.
즉 프록시 객체를 이용한 로딩 방식!
- 지연 로딩 : 필요한 시점에 연관된 데이터를 불러온다
@ManyToOne(fetch = FetchType.LAZY) - 즉시 로딩 : 데이터를 조회할 때 연관된 데이터까지 한 번에 불러온다
@ManyToOne(fetch = FetchType.EAGER)
지연 로딩(LAZY)을 사용해서 프록시로 조회
@Entity
public class Member {
..
@ManyToOne(fetch = FetchType.LAZY) // 지연로딩
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
fetch = FetchType.LAZY 지연 로딩을 세팅하면 엔티티를 조회할 때 연관된 걸 프록시 객체로 가져온다.
(Team team은 프록시 객체로 가져오게 된다)
한마디로 team1 엔티티는 지연(LAZY)로딩으로 세팅이 돼있으므로 프록시 객체로 가져온다!
즉시 로딩(EAGER)을 사용해서 프록시로 조회
JPA는 즉시 로딩을 최적화 하기 위해 가능하면 조인 쿼리를 사용한다
@Entity
public class Member {
..
@ManyToOne(fetch = FetchType.EAGER) //즉시로딩
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
즉시 로딩 실행 SQL에서 JPA가 내부 조인(INNER JOIN)이 아닌 외부 조인(OUTER JOIN)을 사용하는 것을 확인할 수 있는데 이는 NULL 가능성 때문이다. 내부 조인이 외부 조인보다 성능이 좋기에 최적화를 위해 내부 조인을 사용하는 것을 권장하므로 외래키에 NOT NULL 제약 조건 설정을 추가로 해준다.
@Entity
public class Member {
..
@ManyToOne(fetch = FetchType.EAGER) //즉시로딩
@JoinColumn(name = "TEAM_ID", nullable = false)
private Team team;
..
}
다음과 같이 외래키에 NULLL 값을 허용하지 않는다고 JPA에게 알려주는 경우 외부 조인 대신 내부 조인을 사용하게 된다.
프록시와 즉시로딩 주의점
- 가급적 지연 로딩만 사용(특히 실무에서)
- 즉시 로딩을 적용하면 예상하지 못한 SQL문이 발생한다
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다
- 즉시 로딩 기본 페치 전략 : @ManyToOne, @OneToOne (LAZY로 설정 권장)
- 지연 로딩 기본 페치 전략 : @OneToMany, @ManyToMany
김영한님의 팁
- getReference를 실제로 많이 쓰진 않지만 프록시의 매커니즘을 이해해야 즉시 로딩과 지연 로딩에 대해 깊이있게 이해가 가능하다.
- 실무에서는 즉시 로딩 사용을 지양!
- 연관 관계 설정 시 기본적으로 지연 로딩을 선택하고, 필요한 데이터를 효율적으로 가져오기 위해 다음과 같은 세 가지 방법을 고려
- JPQL Fetch Join: 필요한 엔티티를 선택하여 한 번의 쿼리로 데이터를 가져온다. 대부분의 경우 이 방법으로 문제를 해결할 수 있다.
- 엔티티 그래프 기능: 어노테이션을 통해 엔티티 간의 관계를 명시적으로 표현하고, 이를 통해 필요한 데이터를 효율적으로 가져올 수 있다.
- 배치 사이즈 설정: 이 방법은 추가적인 쿼리가 발생하지만, 관련 데이터를 효율적으로 로드하는 데 도움을 준다.
•참고 문헌 : 자바 ORM 표준 JPA 프로그래밍 / 김영한
'JPA' 카테고리의 다른 글
[QueryDSL] 동적 쿼리 게시물 조회 기능 구현 (기본문법) (2) | 2024.06.16 |
---|---|
자바 ORM 표준 JPA 기본편 - 영속성 전이 (0) | 2024.04.24 |
자바 ORM 표준 JPA 기본편 - 고급매핑 (0) | 2024.04.12 |
자바 ORM 표준 JPA 기본편 - 다양한 연관관계 매핑 (0) | 2024.04.04 |
자바 ORM 표준 JPA 기본편 - 연관관계 매핑 기초 (4) | 2024.04.01 |