[강의 정리] 프록시와 지연 로딩

나무·2023년 12월 3일

JPA 

목록 보기
7/11
post-thumbnail

1. 지연 로딩이란?

우리가 게시글을 조회한다고 가정해보자.

게시글 엔티티는 회원 엔티티와 연관관계가 맺어져있긴 하지만 우린 오직 게시글에 대한 정보만을 조회 하고 싶다.

하지만 아쉽게도 POST 테이블을 조회하면 JPA는 우리 몰래 MEMBER 테이블을 모조리 싹다 JOIN 하여 가져온다.

현재는 고작 테이블이 두개이며 안에 컬럼도 얼마 없기 때문에 그러려니 넘어갈 수 있지만,

연관관계를 맺은 테이블이 10개에 다가 각 테이블마져 컬럼이 20개씩 있는 테이블이라고 생각해보자.

우린 고작 게시글에 대한 정보만을 원하는데 사용도 안하는 테이블까지 모조리 긁어와버림으로써 굉장히 비효율적인 상황이 발생 된다.

내가 정말 사용할 시점에만 테이블을 조인해서 가져올 순 없을까?

지연 로딩

JPA 는 당연히 그러한 기능을 지원해준다.

게시글 엔티티의 FetchType 을 LAZY 로 설정만 해주면,

findMember.getName() 을 호출 하기 전까지 MEMBER 테이블 조회를 미루는 것을 확인 할 수 있다.

말 그대로 MEMBER 테이블에 대한 로딩을 지연 한것이다.

그럼 어떻게 이게 가능한것일까???

2. 프록시 기초

프록시 란?

프록시 객체는 원본 객체를 상속받아 원본 대신 작동하는 객체로, 클라이언트 코드가 실제 객체를 직접 사용하는 것처럼 보이도록 하는 객체이다. 주로 지연 로딩(Lazy Loading)이나 퍼포먼스 향상을 위해 사용된다.

프록시 패턴을 어느정도 공부하신분들이라면 이해가 빠르겠지만 오늘 처음본사람일 경우 확 와닿지 않을 수 있기에 그림으로 간단히 프록시를 설명해보겠다. (오히려 역효과일수도..?)

프록시 설명 예시 만화

군대를 다녀온 분들은 아시겠지만, 경계근무는 군생활의 꽃이다. 문제는 초소의 경우 무슨일이 있더라도 24시간 365일내내 군인이 안에서 지키고 있어야한다.

전역이 얼마 남지 않은 말년 김원본은 오늘도 어김없이 경계근무를 서게 되었다. 하지만 짬이 찰대로 찬 김원본은 잔머리를 굴려 본인과 똑같이 생긴 허수아비 로봇(프록시 객체)을 초소에 세워놓고 본인은 뒤에서 몰래 취침을 할 생각이다.

꿀잠을 청하고 있던 김원본에게 위기가 찾아왔다. 부대에 원스타가 아무 말도 없이 방문한 것이다. 그것도 김원본의 초소로 말이다.

하지만 다행히 김원본은 자신이 만든 프록시에게 누군가 방문을 할 경우 자신을 깨워줄 버튼을 누르도록 간단한 프로그래밍을 해놓았고 다행히 프록시는 문제없이 원본을 호출하였다.

원스타는 연합사 사무실이 어디냐고 프록시한테 물었다. 원본이는 당황하지 않고 무전기로 응답을 했고 미리 초소에 설치해둔 스피커를 통해 원본이의 목소리가 제대로 흘러 나왔다.

다행히 원스타는 프록시인걸 전혀 눈치채지 못했고 원본이는 가뿐히 상황을 모면할 수 있었다.

원본이는 프록시를 치우고 본인이 직접 경계 근무를 서기 시작한다.

위 만화의 상황을 이제 실제 JPA 사용과 비교하면서 설명을 해보겠다.

프록시 = 프록시 객체
김원본 = 원본 엔티티 객체
초소 = 영속성 컨텍스트
원스타 = 클라이언트

1) 원본 대신 프록시가 경계근무를 선다.
target = null
-> 실제 원본 엔티티가 미리 로딩되어있을 필요가 없다.

2) 원스타가 방문했을때 프록시가 원본 호출 버튼을 누른다
target = realEntity;
-> 실제 사용 시점에 프록시는 초기화 요청을 하고 실제 엔티티가 생성이된다.

3) 원스타는 아무 이상함을 눈치채지 못한다.
target.getName()
-> 클라이언트는 본인이 조회한 데이터가 프록시가 준건지, 원본 엔티티가 준건지 알 필요가 없다.

4) 원본이가 초소로 다시 복귀
-> 이제 영속성 컨텍스트에는 실제 엔티티가 저장이 되어있다.

100% 는 아니지만 얼추 비유가 들어맞긴한다.


프록시 실제 동작

이제 실제 프록시가 어떻게 동작하는지 한번 설명을 해보겠다. 예시는 맨위에서 지연로딩에 사용했던 테이블 (POSTMEMBER) 그대로 재활용 한겠다.

1) 클라이언트는 게시글을 조회한다.

그럼 JPA(하이버네이트) 는 DB를 통해 게시글에 대한 정보를 가져오며 Post 엔티티를 생성한다.

2) 프록시 참조

지연 로딩 전략을 사용하기 때문에 Post가 가지고 있는 Member 는 실제엔티티가 아닌 프록시 객체를 참조하고 있다.


3) 초기화 시작

앞서 설명했듯이 findMember 는 프록시 객체이다. 프록시 객체는 호출 되는 시점에 초기화를 진행하며 단 한번만 수행된다.

헷갈릴 수 있는데, 초기화한다는 의미는 프록시 인스턴스를 생성한다는 것이 아니다. DB로 부터 데이터를 조회하여 실제 엔티티를 생성 하고 프록시가 그것(target) 을 참조 하게된다는 의미이다.

※ getName() 이 호출 될 때, 인터셉터가 바로 실행되면서 초기화를 시작함.

※ 생성된 실제 엔티티는 영속성 컨텍스트에 저장된다.


4) 게시글 작성자 이름 출력

프록시는 target(실제엔티티) 에게 getName() 동작을 위임한다 (리플렉션을 활용해 메서드를 동적 호출함).

※ 만일 동일 트랜잭션 내에서 한 번 더 getName() 을 호출한다면 그땐 프록시가 아니라 영속성 컨텍스트에 저장된 실제 엔티티를 통해 직접 호출된다.


프록시 정리

  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.

  • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는것은 아니고, 실제 엔티티에 접근 할 수 있게된다.

  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 프록시가 아닌 실제 엔티티를 반환한다. em.getReference() 를 사용해도 마찬가지이다.

  • 초기화는 영속성 컨텍스트의 도움을 받아야 가능하기 때문에 준영속 상태의 프록시를 초기화 하면 예외가 발생한다.
    LazyInitializationException

※ em.getReference()

엔티티를 실제 사용하는 시점까지 데이터베이스 조회가 미루어지며 프록시 객체를 반환해준다.

우리가 직접 사용할 일은 거의 없다.


프록시와 식별자

만일 회원 프록시 객체를 통해 이름이 아닌 식별자(ID) 를 조회한다면 어떻게 될까?

결론 부터 말하자면, 이때는 초기화가 진행되지 않는다. 왜냐하면 프록시는 이미 식별자 값을 가지고 있기 때문이다.

이를 응용하면 엔티티끼리 연관관계를 맺어줄때,

Member member = em.find (Member, class, "memberl");
Team team = em.getReference (Team.class, "teaml"); //SQL을 실행하지 않음
member.setTeam(team);

위와 같이 하여 DB접근 횟수를 줄일 수 있다.

본 포스트는
김영한의 자바 ORM 표준 JPA프로그래밍 기본 강의 및 도서를 참고하여 정리했습니다.

profile
🍀 개발을 통해 지속 가능한 미래를 만드는데 기여하고 싶습니다 🍀

0개의 댓글