일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- JavaScript
- 삼성 SW 역량테스트
- dp
- 컴퓨터 구조
- 그리디
- 다익스트라
- 우선순위 큐
- REACT
- 수학
- 백준 1753번
- 삼성SW역량테스트
- BFS
- 생활코딩
- r
- 접미사 배열
- jpa
- 시뮬레이션
- Cloud Run
- ICPC
- LCS
- 고속 푸리에 변환
- 다이나믹 프로그래밍
- 이분탐색
- 펜윅 트리
- Cloud Pub/Sub
- 데이터 분석
- CI/CD
- 종만북
- Bit
- Air Table
- Today
- Total
코딩스토리
프록시를 사용한 즉시 로딩, 지연 로딩 본문
# 해당 포스팅은 인프런 김영한 강사님의 "자바 ORM 표준 JPA 프로그래밍 - 기본편" 강의 및 교재를 참고하여 작성한 글입니다.
프록시
프록시란 가짜를 의미한다.
JPA 뿐만 아니라 프록시 서버, 프록시 객체, Spring의 싱글톤에서의 CGLIB 등 여러 방면에서 프록시란 용어가 사용된다.
JPA에서의 프록시란 데이터베이스 조회를 미루는 프록시 엔티티 객체를 말한다.
JPA에서는 왜 프록시를 사용할까?
이유는 다음과 같다.
만약 Member라는 클래스와 Team이라는 클래스가 아래와 같은 연관관계를 가지고 있다고 가정하자.
이때 Member 객체는 Team값을 참조하고 있다.
즉 Member 테이블은 FK 값으로 team_id를 가지고 있고, JPA에서 Member 클래스를 find 할 때 DB의 Team 테이블과 join을 해서 team에 대한 값을 가져온다는 것이다.
(이 부분이 이해가 어려울 수 있다.
먼저 JPA가 DB의 테이블과 JAVA 단의 객체와 매핑하는 부분에 대해 알아야 한다. => 연관관계 매핑 공부하기
현재는 Member와 Team이 단방향 ManyToOne 매핑이 되어있는 상태이다.)
근데 만약 나는 Member의 username만 필요하고 Team에 대한 정보는 관심이 없다면?
JDBC 프로그래밍에서는 query를 수정하면 되지만 JPA는 알아서 join 쿼리를 만들어서 날리기 때문에 이러한 부분에서 비효율적이다.
이런 비효율적인 부분을 개선하기 위해 프록시가 사용된다.
프록시의 사용
만약 일반적으로 em.persist를 통해 DB에 저장하고, 해당 객체(예제에서는 Member 클래스)를 find로 가져오면 아래와 같이 Entity 명과 같은 클래스의 객체를 반환한다.
JPA에서 프록시 객체를 가져오는 방법은 em.getReference() 함수를 사용하면 된다.
이때 em.getReference() 함수를 통해 가져온 객체의 클래스는 아래와 같다.
Member 뒤에 Proxy라고 명시되어 있음을 확인할 수 있다.
즉 실제 객체가 아닌 가짜 Entity 객체를 반환해준다.
이 프록시 객체는 실제 Member Entity를 상속받아서 만들어진다.
대신 안에는 텅텅 비어있는, 깡통 객체이다.
이 객체는 아래와 같은 과정을 통해 채워진다.
여기서 주의 깊게 봐야 될 부분은 MemberProxy의 Member target 부분이다.
즉 실제 Member에 대하여 target으로 참조하고 있다가 실제 데이터가 필요할 시(get 호출 시)
영속성 컨텍스트에 요청을 하고, 없다면 DB를 통해 조회를 한 뒤 실제 Entity를 생성해온다.
이후 호출했던 get 함수를 실행한다.
이 외에도 중요한 특징들이 있다.
1. 초기화 요청은 처음 사용할 때 한 번만 호출된다.
2. 프록시 객체가 실제 Entity로 변경되는 것이 아니라 참조값이 채워짐으로써 프록시 객체를 통해 실제 Entity에 접근 할 수 있게 되는 것이다.
3. 프록시 객체는 실제 Entity를 상속받은 것이기 때문에 == 비교가 아닌 instance of를 사용해야 한다.
다음으로 실무에서 많이 만나게 된다는 에러에 대해 알아보자.
프록시 초기화 시, 만약 영속성 컨텍스트를 비우거나 detach 등을 통해 프록시 객체를 영속성 컨텍스트가 관리하지 않는 준영속 상태로 만들게 된다면 아래와 같은 에러를 발생한다.
org.hibernate.LazyInitializationException: could not initialize proxy [hellojpa.Member#1] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:169)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:309)
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
at hellojpa.Member$HibernateProxy$leXzhvJ6.getName(Unknown Source)
at hellojpa.JpaMain.main(JpaMain.java:33)
4월 14, 2022 9:07:32 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/test]
왠지 조만간 만나게 될 것 같아서 혹시 몰라 남겨둔다.. ㅎ
해당 에러가 발생하는 이유는 위의 그림에서 찾아낼 수 있다.
위의 그림에서 보면 프록시 객체를 초기화할 때 반드시 영속성 컨텍스트를 거치게 된다.
이때 준영속 상태(영속성 컨텍스트에서 프록시 객체를 관리하지 않는 상태)가 된다면.. 당연히 초기화가 불가능할 것이다.
즉시 로딩과 지연 로딩
그럼 항상 getReference() 함수를 사용해서 프록시 객체를 만들고, 초기화하고 등등 이런 귀찮은 부분들을 해야 하는 걸까?
당연히 개발자들은 언제나 그렇듯 편한 방법을 개발해왔다.
다음의 코드를 보자.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
@ManyToOne 어노테이션 옆에 'fetch = FetchType.LAZY'라는 특성을 줬다.
직역 그대로 지연 로딩을 하겠다는 의미이다.
이는 쉽게 풀어 말하면 해당 부분(Team에 대한 정보)은 프록시 객체를 사용하여 있는 척만 하고 나중에 실제로 필요할 때(지연) 불러오겠다는 의미이다.
프록시를 이해했다면 해당 부분이 쉽게 이해가 될 것이다.
이와 반대의 경우도 당연히 있겠죠?
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
이번엔 FetchType.EAGER을 사용하였고, 이는 즉시 로딩하겠다는 의미이다.
쉽게 풀어써서 말하면 Member 객체를 가져올 때 Team도 같이 가져오겠다는 의미이다.
만약 서비스의 비즈니스 로직에서 Member와 Team을 같이 사용하는 경우가 많다면 Eager 옵션을 사용하면 된다.
그 반대라면 LAZY를 사용하면 된다.
라고 했지만 실무에서는 가급적 지연 로딩만 사용해야 한다고 한다.
왜냐하면 즉시 로딩 사용 시 예상하지 못한 SQL이 나간다.
여러 개의 테이블이 연관관계를 가지고 있을 시 즉시 로딩을 사용하게 된다면 매 번 모든 테이블을 조인하는 쿼리가 나가게 되고 성능에 큰 문제를 일으킬 수 있다고 한다.
이 외에도 JPQL 사용시 N+1 문제 (Team을 조회하게 되면 Team에 속한 Member들을 또 즉시 로딩으로 조회하고 쿼리가 매우 많이 나가게 되면서 이 부분 때문에 성능 저하) 등 다양한 문제가 발생한다고 한다.
따라서 fetch Join, 어노테이션 등 다른 방법을 사용하여 해결한다고 하는데 이는 나중에 포스팅할 예정이다.
실제 강의에서는 강력하게 지연 로딩을 사용하라고 말하셨습니다..!
경험에서 나오는 말들은 항상 옳았기 때문에 믿습니다...! ☺