프록시
등장배경
Member를 조회할 때 Team도 함께 조회해야할까?
회원과 팀 함께 출력
public void printUserAndTeam(String memId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원이름 : " + member.getUsername());
System.out.println("소속팀 : " + team.getName());
}
회원만 출력
public void printUser(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름 : " + member.getUsername());
}
비즈니스 상황에 따라 다르다
주로 회원만 출력하는 경우, 굳이 Team까지 같이 가져올 필요 없다.
하지만, Member와 Team을 같이 출력하는 비즈니스 로직이 많을 경우, Member 따로 Team 따로 쿼리날려서 조회하는 것보다는 Member를 조회할 때 연관된 Team까지 한번에 조회해오는 것이 성능상 좋다.
JPA는 이러한 고민을 지연로딩과 프록시 기술을 사용하여 해결한다.
프록시 기초
em.find() vs em.getReference()
- em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회 (DB에 쿼리가 안나가는데 객체 조회가 된다.)
- 엔티티 객체와 겉은 똑같은데 속은 비어있다.
- 내부에 target이라는 필드가 있는데, 이게 실제 엔티티객체를 가리킨다.
프록시 특징
- 실제 클래스를 상속받아서 만들어진다. (hibernate가 내부적으로 proxy 라이브러리들을 사용해서 만들어낸다.)
- 그래서 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다. (이론상)
- 프록시객체는 실제 객체의 참조(target)을 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
- 하지만 처음에는 target에 참조값이 없다. (아직 DB 조회하지 않았으니까)
프록시 객체의 초기화
Member member = em.getReference(Member.class, "id1");
member.getName();
동작 흐름
- 처음에 member.getName() 호출
- 가져온 프록시 객체의 target이 null이다.
- JPA가 (정확히 말하면 hibernate가) 영속성 컨텍스트에 초기화 요청을 한다.
- 영속성 컨텍스트는 DB조회해서 실제 Entity 생성해서 반환한다.
- 그리고 프록시 객체의 target에 반환된 실제 Entity 참조값 저장한다.
- target.getName()이 호출되면서 원하는 로직 실행된다.
프록시 특징 정리
- 프록시 객체는 처음 사용할 때 한번만 초기화한다.
- 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근가능한 것일 뿐이다. (target 필드 초기화)
- 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야한다. (==비교를 하면 실패한다, 대신 instance of 사용하면된다.)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다. (왜냐하면 JPA에서는 같은 트랜잭션안에서는 같은 영속성 컨텍스트에서 조회되는 엔티티의 동일성 보장해야한다. == 비교 항상 참이 되어야한다.)
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속상태일 때, 프록시를 초기화하면 문제가 발생한다.
- hibernate는 org.hibernate.LazyInitializationException 예외를 터뜨린다.
- 엔티티가 준영속성 상태가 되는 대표적인 경우는 트랜잭션이 종료되어서 영속성컨텍스트가 close된 경우이다.
- 실무에서 트랜잭션이 끝나고 영속성 컨텍스트를 조회하는 경우에 많이 마주치는 오류
프록시 확인
프록시 인스턴스의 초기화 여부 확인
- PersistenceUnitUtil.isLoaded(Object entity)
프록시 클래스 확인 방법
- entity.getClass().getName() 출력
- (..javasist.. or HibernateProxy...)
프록시 강제 초기화
- org.hibernate.Hibernate.initialize(entity)
- 참고 : JPA 표준은 강제 초기화 없음
- 강제 호출 : member.getName()
즉시로딩과 지연로딩
다시 처음으로...
Member를 조회할 때 Team도 함께 조회해야 할까?
지연로딩
단순히 member정보만 사용하는 비즈니스 로직
println(member.getName());
- 연관관계 걸려있다고해서 Team까지 조회해오면 손해다.
- 이를 위해 JPA는 지연로딩이라는 옵션을 제공한다.
지연로딩 LAZY를 사용해서 프록시로 조회
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team
...
}
내부 동작 방식
- member1 조회시 연관된 Team 엔티티는 DB에서 조회해오지 않고 프록시로 초기화해준다.
- Member 조회시 Team 까지 같이 조회하지 않고 Lazy Loading 한다 = 프록시로 초기화한다.
- 실제로 team 엔티티를 사용하는 시점 (team.getName()호출 시점)에 프록시가 실제 엔티티 가리키도록 초기화한다. (DB에서 조회, 쿼리 날라감)
즉시 로딩
비즈니스 로직 상 Member와 Team을 자주 함게 사용한다면?
한번에 같이 조회해오는 것이 좋다. 네트워크 여러 번 탈 이유가 없다.
즉시 로딩 EAGER를 사용해서 함께 조회
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
- 즉시로딩으로 조회시 Member 조회할 때 Team도 함께 조회
- 따로 따로 쿼리날리지 않고 join해서 한번에 가져온다.
- 애초에 엔티티를 함께 조회해서 가져오기 때문에 Team 엔티티도 프록시가 아닌 실제 엔티티를 가져온다.
내부 동작 방식
- member 조회시 team까지 같이 join해서 가져오는 것
- JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회하도록 구현한다.
- EAGER 즉시 로딩 하는 방법 2가지
- 처음에 조회할 때 join해서 한번에 가져오기 : 가능한한 이방식
- 일단 member 가져온 후 team 조회해서 넣어주기 : 위의 방식 불가능한 경우
프록시와 즉시로딩 주의
- 가급적 지연 로딩만을 사용 (특히 실무에서)
- 즉시로딩을 적용하면 예상하지 못한 SQL이 발생
- 실무에서 테이블에 연관관계 여러 개 걸려있다고 하면 EAGER로 되어있을 때 join 여러 개 발생한다. ➡ 성능문제
- table 막 10개랑 연관되어있다하면 EAGER 로딩시 table 하나 조회하는데 table 10개 다 끌고온다.
- 즉시 로딩은 JPQL에서 N+1문제를 일으킨다.
- 실무에서는 복잡한 검색조건으로 조회 위해 jpql 많이 사용
- 그냥 em.find()하면 JPA가 조인으로 쿼리 최적화해서 가져오지만, jpql은 sql로 번역되어서 나가기 때문에 join되서 가져오지 않는다.
- EAGER 로딩으로 설정되어있으면 일단 member 엔티티 조회해온후 연관된 엔티티 다시 조회해서 채워준다.
- 추가 쿼리가 발생한다 (추가로 채워줘야할 데이터 개수 N개 만큼 쿼리 발생) ➡ 처음 날린 쿼리 1 + 결과값들 쿼리 N : N+1 문제
- @ManyToOne, @OneToOne은 기본이 즉시로딩 ➡ LAZY로 설정할 것
- @OneToMany, @ManyToMany는 기본이 지연로딩
지연 로딩 활용
이론
여기서 나오는 것은 다 이론적인 것! 실무에서 이렇게하면 안돼👿
실무에서는 무조건 지연 로딩으로 다 설정해야한다!!
- Member와 Team은 자주 함께 사용 ➡ 즉시 로딩
- Member와 Order는 가끔 사용 ➡ 지연 로딩
- Order와 Product는 자주 함께 사용 ➡ 즉시 로딩
- LAZY로 걸려있는 것은 프록시로 초기화한다.
- 프록시 객체 실제로 호출되면 그때 실제 엔티티로 초기환된다.
실무
- 모든 연관관계에 지연 로딩 사용해라!!
- 실무에서 즉시 로딩 사용하지 마라!!
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라!
- 즉시 로딩은 상상하지 못한 쿼리가 나간다!!
해당 게시글은 인프런 김영한님의 <자바 ORM 표준 JPA 프로그래밍 - 기본편>을 듣고 정리한 내용입니다.