도입
정말 오랜만에 JPA와 관련된 글을 작성하는 듯하다. 약 2주정도는 갑작스럽게 영어 스피킹 시험을 보고 싶다는 핑계로 JPA에 대한 글을 잠시 미루고 열심히 영어에 매진하였다. 결과는 망했지만.
이번에 우리는 JPA의 프록시 객체와 지연로딩에 대하여 알아보려고 한다.
프록시 객체?
Proxy는 번역하면 대리라고 할 수 있다. 남을 대신하는 무엇인가를 프록시라고 한다. 프록시 객체는 말 그대로 객체를 대신한다는 의미를 포함하고 있다. 객체를 대신한다는 말이 무엇을 의미할까. DB에 있는 값을 영속성컨텍스트로 관리하려면 find()를 사용한다. find를 부르게 되면 영속성 컨텍스트에 값이 있는지 확인하고 없다면 DB로 쿼리가 날라가서 값을 가져올 것이다.
find가 아닌 다른 방법으로 DB에 있는 객체에 접근하려고 한다. 즉 DB에 있는 값을 참조하는 객체를 만들어주어 이 값을 사용하는 것이다. EntityManager.getReference(~)를 사용하게 되면 프록시 객체를 생성할 수 있다. 그렇게 생성한 프록시 객체는 find로 불러온 객체와 동일한 역할을 수행하며 동일한 객체가 된다.
그렇다면 find와 getReference의 차이는 무엇일까? 매우 단순하게 생각하면 DB에서 값을 가져오는 시점이 다르다. find를 사용하면 바로 DB에서 값을 가져와 사용을 하게 되지만, getReference는 직접 참조하여 값을 사용하기 전에는 먼저 DB에서 값을 가져오지 않는다.
초기화가 되는 경우는 사용자가 해당 객체를 사용하게 되는 경우다. 객체가 사용이 되어 DB로 쿼리를 날리고 영속성 컨텍스트에서 관리를 하게 되어도 해당 객체는 프록시 객체로 남아 있으며 프록시에서 target 객체에게 요청하여 사용되는 식으로 이용된다.
특징?
- 프록시 객체를 사용해도 일반 객체를 사용하듯이 사용하면 된다.
- 가장 큰 특징은 값을 직접 사용하기 전에는 DB로 쿼리가 날라가지 않는다는 것이다.
- class를 확인해보면 Proxy임을 알 수 있다.
- 동일한 객체에 대하여 == 비교난 항상 true를 보장한다.
instance of를 사용하자
프록시 객체의 클래스를 찍어보면 우리가 알고있는 객체의 클래스가 아닌 다른 Proxy관련 객체가 나오게 된다. 따라서 class값을 비교할 때 == 비교를 하게 된다면, false 값이 나오게 된다. 따라서 instace of를 활용해 클래스 비교를 해주어야한다. 이 때 Proxy의 객체가 상속으로 구현이 되기 때문에 instace of로 비교가 가능한다.
같은 객체에 ==
같은 객체에 대하여 == 연산자는 항상 true를 반환한다. 만약에 동일한 객체를 find와 reference로 반환해주었을 때도 두 객체를 == 연산해주면 항상 true가 된다.
그렇게 해주기 위해서는 같은 클래스로 처리가 되어야한다. 즉 find와 getReference로 불러진 객체의 클래스가 동일해야한다.
Member findMember = em.find(Member.class, member.getId());
Member refMember = em.getReference(Member.class, member.getId());
find를 먼저 수행하게되면 DB로 쿼리가 날라가게 되고, 영속성 컨텍스트에서 관리를 하게된다. findMember의 class는 일반 객체인 Member의 클래스가 된다. 이때 refMember도 동일하게 일반 객체로 반환이 된다.
Member refMember = em.getReference(Member.class, member.getId());
Member findMember = em.find(Member.class, member.getId());
만약 반대로 프록시 객체를 먼저 사용하면, find로 생성한 객체도 프록시 객체가 return된다.
지연로딩과 즉시로딩
JPA에서 객체를 찾아서 사용하기 위해서 find를 사용하여 부를 수 있다. 이 때 만약 find한 객체가 영속성컨텍스트에서 관리되고 있는 객체라면 영속성 컨텍스트에서 관리되는 것을 사용하지만, 없다면 DB로 쿼리를 날려 가져오게 된다.
이 때 연관되어있는 또 다른 객체들은 어떻게 해야할지를 정해주는 것이 지연로딩과 즉시로딩이다. 즉시로딩은 말 그대로 받아올 때 함께 받아오는 것을 의미한다. 따라서 객체를 가져올 때 Join을 수행하여 1번의 쿼리만으로 데이터를 모두 가져온다. 지연로딩은 프록시 객체로 받아서 추후에 그 객체를 사용할 때 DB에서 부르게 된다. 만약 Team과 Member가 연관관계가 있다고 하자. 그리고 비즈니스 로직상 Member를 사용할 때 항상 Team을 사용한다면 즉시로딩을 사용하는 것이 효과적일 것이다. Member만 사용하는 경우가 많다면 지연로딩이 효과적일 것이다.
지금까지 말한 내용은 사실 굉장히 이론에 입각한 이야기이다. 실무에서는 즉시로딩은 절대 사용을 할 수 없으며 반드시 지연로딩만 사용해야한다. 그 이유는 즉시로딩을 사용하면 굉장히 치명적인 문제들이 많기 때문이다.
즉시로딩의 단점
- 내가 예상하지 못한 쿼리가 나가게 된다.
즉시로딩은 쿼리를 예측할 수 없다. 왜냐하면, 즉시로딩으로 설정이 되어있는 모든 값들을 가져와야하기 때문이다. 만약 10개의 객체가 연관되어있다면, 즉시로딩으로 실행을 하면 10개에 대한 정보를 모두 가져오게 되는 쿼리를 날려 문제가 발생한다. - N+1문제가 발생한다.
N+1문제는 JPQL을 사용할 때 발생할 수 있다. 만약 "select * from Member"를 JPQL로 날렸다고 하면, 이것은 sql로 변환되어 Member를 모두 가져오는 쿼리를 날리게 된다. 하지만 Member가 Team을 즉시로딩으로 가져오게 된다면, Member를 보고 그에 따른 Team에 대한 정보를 모두 가져오는 쿼리가 날라간다. Team의 개수가 N이라고 하면 "select * from Member" 이것을 한번 실행하기 위해 N번의 Team을 찾는 쿼리가 날라간다고 하여 N+1문제라고 부른다. 즉 JPQL을 사용할 때 치명적인 문제가 발생한다.
지연로딩을 설정하는 방법
지연로딩과 즉시로딩은 연관관계를 설정한 annotation의 fetch 값을 변경하여 설정해줄 수 있다.
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
즉시로딩을 설정하는 것은 EAGER값을 설정해주면 된다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
지연 로딩을 설정하는 방법은 LAZY로 설정해주면 된다.
위에서 알아본 문제들 때문에 즉시로딩은 현재 스펙상으로는 사용이 굉장히 제한된다. 따라서 지연로딩으로 설정을 해주어야한다. @ManyToOne 과 @OneToOne의 경우 fetch의 default 값이 EAGER이므로 반드시 지연로딩으로 설정을 해주어야 한다. 그 외 다른 값들은 default가 LAZY이기 때문에 변경할 필요는 없다.
이 글은 김영한님의 강의 "자바 ORM 표준 JPA 프로그래밍"를 공부한 후 작성한 글 입니다.
'Back-end > JPA' 카테고리의 다른 글
JPA 상속관계 매핑하기(+ @MappedSuperclass) (0) | 2022.01.16 |
---|---|
JPA 연관관계 매핑하기 (0) | 2022.01.15 |
JPA 기본 키 매핑하기 (0) | 2022.01.12 |
JPA로 필드와 칼럼 매핑하는 annotation 알아보기 (0) | 2022.01.10 |
JPA로 객체와 테이블 매핑하기 (0) | 2022.01.07 |