[Spring]JPA Proxy & LAZY & EAGER

김피자·2023년 3월 24일
0

etc.

목록 보기
3/10
post-thumbnail

스프링에서는 hibernate, Mybatis 등 다양한 ORM프레임워크를 지원한다.
이 ORM 프레임워크는 객체와 데이터베이스 간 매핑을 담당해 개발자가 데이터 베이스에 대한 복잡한 접근 코드를 줄일 수 있도록 한다.

JPA

JPA에서 테이블 간 연관 관계는 객체의 참조를 통해 이루어진다.
실무에서는 서비스의 규모가 크기 때문에, 데이터의 양이 많은 것은 물론 각각의 데이터들끼리 참조하고 있어 연관된 데이터들을 한번에 가져오는 것은 굉장히 부담이 크다.

엔티티들은 데이터 베이스에 저장되어 있기 때문에 한 객체 조회 시 연관 된 엔티티를 모두 조회하는 것 보다는 필요한 연관 관계만 조회해 오는 것이 효과적인데 이런 상황을 위해 JPA는 지연 로딩이라는 방식을 지원한다.

JPA는 참조하는 객체들의 데이터를 가져오는 시점을 정할 수 있는데 이것을 Fetch Type이라 한다.
Fetch Type에는 Lazy 지연로딩과 Eager 즉시로딩이 있다.

지연로딩 이란?
자신과 연관된 엔티티를 실제로 사용할 때 연관된 엔티티를 조회(SELECT)하는 것을 말한다.

즉시로딩 이란?
엔티티를 조회할 때 자신과 연관되는 엔티티를 조인(JOIN)을 통해 함께 조회하는 방식을 말한다.


JPA Proxy

Fetch Type에 차이에 대해 알아보기 전, JPA Proxy가 무엇인지부터 알아보자
먼저, 프록시는 '대신하다'라는 의미로 어떤 동작을 대신해주는 가짜 객체라고 생각하면 된다.

위에 설명햇듯 Hibernate는 프록시 객체를 통해 지연 로딩을 구현하는데 지연 로딩을 하려면 연관된 엔티티의 실제 데이터가 필요할 때 까지 조회를 미뤄야한다.
그렇다고 해당 엔티티를 연관 관계로 가지고 있는 엔티티의 필드에 null 값을 넣어 둘 수 없으니 지연 관계의 연관 자리에 프록시 객체를 주입해 실제 객체가 들어있는 것 처럼 동작하도록 한다.
덕분에 우리는 연관관계 자리에 프록시 객체가 들어있던 실제 객체가 들어있던 신경쓰지 않고 사용할 수 있다.


Proxy는 어떻게 실제 객체처럼 동작할까?

그렇다면 프록시는 어떻게 실제 객체가 들어있는 것 처럼 동작할 수 있을까?
바로 프록시가 실체 객체를 상속한 타입을 가지고 있기 때문이다.
프록시 객체는 실제 객체에 대한 참조를 보관하여, 프록시 객체의 메소드를 호출 했을 때 실제 객체의 메소드를 호출한다.
그렇기 때문에 실제 객체 타입의 자리에 들어가도 아무 문제 없이 사용할 수 있는 것이다.


Proxy의 초기화

최초 지연 로딩 시점에는 참조 값이 없는 상태이다. 실제 객체의 메소드를 호출할 필요가 있을 때 데이터베이스를 조회해 참조 값을 채우게 되는데 이를 두고 프록시 객체를 초기화한다라고 말한다.


LAZY vs EAGER 무엇을 사용하는 것이 좋을까?

보편적으로는 지연(Lazy)로딩을 사용한다고 한다.
즉시(Eager)로딩은 관련되어있는 모든 객체를 가져오기 때문에 불필요한 조인으로 인한 성능저하를 피하기 어렵다.

한 예로 한 유저가 작성한 1000개의 글을 조회할 때, 글 작성자를 찾기 위해 1000개의 유저를 찾는 쿼리도 함께 발생할 필요가 있을까? 보통 이문제를 n+1문제라 하는데 개발자가 예상한 것보다 2배의 쿼리가 진행되어 큰 비용 손실이 발생하게 된다.

Team team = new Team();
team.setName("teamA");
em.persist(team);

User user = new User();
user.setUsername("user1");
user.setTeam(team);
em.persist(user);

em.flush();
em.clear();

User findUser = em.find(User.class, user.getId());

Team 객체와 User 객체를 각각 만들어 값을 셋팅해주고 em.find() 메소드를 통해 User를 조회했다.

여기서 중요하게 볼 것은 우리는 User를 조회했다는 점이다.

EAGER일 때,

Hibernate: 
    select
        user0_.USER_ID as USER_I1_0_0_,
        user0_.TEAM_ID as TEAM_ID3_0_0_,
        user0_.USERNAME as USERNAME2_0_0_,
        team1_.TEAM_ID as TEAM_ID1_1_1_,
        team1_.name as name2_1_1_ 
    from
        User user0_ 
    left outer join
        Team team1_ 
            on user0_.TEAM_ID=team1_.TEAM_ID 
    where
        user0_.USER_ID=?

User를 조회했는데 Team 테이블까지 Join되어 쿼리문이 실행된 것을 볼 수 있다.

LAZY일 때,

Hibernate: 
    select
        user0_.USER_ID as USER_I1_0_0_,
        user0_.TEAM_ID as TEAM_ID3_0_0_,
        user0_.USERNAME as USERNAME2_0_0_ 
    from
        User user0_
    where
        user0_.USER_ID=?

Lazy로 fetch type을 바꾸니 필요한 User 테이블만 조회하는 것을 볼 수 있다.

findUser.getTeam().getName();

그리고 위처럼 User 객체로부터 Team 객체의 데이터를 요구하는 코드를 만나면

Hibernate: 
    select
        team0_.TEAM_ID as TEAM_ID1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?

Team 데이터를 조회하는 쿼리문이 실행되는 것을 볼 수 있다.


LAZY & EAGER 선택하여 쓰기

Eager는 User를 조회하면 연관 관계에 있는 Team까지 함꼐 조회하는 반면, Lazy는 User만 조회하고 연관 관계에 있는 나머지 데이터는 죄회를 미룬다.

비지니스 로직 상 User 데이터가 필요한 곳 대부분에 Team의 데이터 역시 같이 사용 할 필요가 있다면 EAGER로 설정하여 항상 User와 Team을 같이 조회해오는 것이 더 좋다.

User를 사용하는 곳 대부분에서 Team 데이터가 필요하지 않다면 FetchType을 LAZY로 설정하여 User만 조회하고, Team이 필요할 땐 그 때 Team에 대한 쿼리를 한번 더 날려 조회하는 것이 좋을 것이다.


그치만 LAZY를 쓰자

실무에서는 보통 EAGER 로딩을 사용하지 않는 것을 권장한다고 한다.

그 이유는 위에서도 말햇지만! 중요하니깐 한번 더 말하면 n+1문제 때문이다!

EAGER는 Jpql로 전달되는 과정에서 Jpql 후 EAGER 감지로 인한 N쿼리가 추가로 발생하는 경우가 있기 때문에 엑스 엑스

끝!!


참조
https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85
https://tecoble.techcourse.co.kr/post/2022-10-17-jpa-hibernate-proxy/
https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85
https://velog.io/@jin0849/JPA-%EC%A6%89%EC%8B%9C%EB%A1%9C%EB%94%A9EAGER%EA%B3%BC-%EC%A7%80%EC%97%B0%EB%A1%9C%EB%94%A9LAZY

profile
제로부터시작하는코딩생활

0개의 댓글