JPA의 프록시 관련 정리
- Spring/JPA
- 2021. 11. 22.
JPA의 프록시를 왜 사용해야할까?
// 이 경우는 TEAM + MEMBER 둘다 필요.
Member member = em.find(Member.class, memberId)
Team team = member.getTeam();
system.out.println("회원 이름 : " + member.getUsername());
ststem.out.println("소속 팀 : " + team.getName());
// 이 경우는 회원만 필요함.
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
system.out.println("회원 이름 : " + member.getUsername());
Member와 Team의 연관관계가 걸려있는 경우가 있다고 가정해보자. 예를 들어 Member의 userName만 출력을 해야하는 경우가 있는데, 이 때 Team까지 함께 조회가 되어야하는 걸까? 만약 매번 JPA에서 이렇게 객체를 전부 가져올 경우, 이는 최적화 관점에서 손해가 될 수 있다. 왜냐하면 Team은 Member가 불려질 때 마다 항상 필요한 것이 아니기 때문이다. JPA는 프록시의 지연로딩 등을 이용해서 이런 것들을 최적화 해줄 수 있다.
프록시의 기초
- em.find() : DB에서 조회해서, 실제 엔티티를 영속성 컨텍스트로 반환해준다. 조회할 때, 바로 select 쿼리가 나간다.
- em.getReference() : DB 조회를 미루는 가짜(프록시) 엔티티 객체를 조회함. 이 때, DB에 쿼리가 안 나갔는데 조회가 된다. 실제 select 쿼리는 엔티티가 필요할 때 나간다.
em.getReference()를 하면 진짜 엔티티를 반환하는 것이 아닌 가짜 프록시 엔티티를 반환해준다. 이 프록시 엔티티에 대한 특성은 아래에서 간략히 정리한다.
프록시 객체의 특징
- 프록시 객체는 실제 클래스를 상속 받아서 만들어진다. 즉, 겉모양이 같다.
- 사용자 입장에서는 진짜 객체, 프록시 객체를 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체의 참조(target)을 보관한다. 프록시 객체가 실제로 호출되는 시점에 프록시 객체는 실제 객체의 메소드를 호출한다.
- Hibernate가 내부 라이브러리를 사용해 DB 조회를 미루는 프록시 엔티티 객체를 준다. Target 값을 가지는데 초기값은 null이다.
특징 코드 확인 : 프록시 객체는 실제 클래스를 상속받아 같은 모양이 만들어진다.
Member member = new Member();
member.setUserName("memberA");
em.persist(member);
em.flush();
em.clear();
Member reference = em.getReference(Member.class, member.getId());
System.out.println(reference.getClass());
System.out.println("reference.getId() = " + reference.getId());
위 코드를 실행해본다. 위 코드는 member를 실제로 선언해서 DB에 저장 후, 프록시 객체를 가져온 후 가져온 객체의 타입을 실제로 출력해보는 코드이다. 실제로 코드를 출력했을 때, 프록시 객체인 것이 보이면 된다.
실행 결과를 확인하면 위와 같다. 먼저 Insert Query가 나간 후, Select 쿼리가 나가지 않았다. em.getReference()로 프록시 객체를 가져왔지만, 실제 엔티티를 가져오기 위한 Select 쿼리는 나가지 않은 것을 확인할 수 있다. 가져온 프록시 객체의 클래스 타입을 확인했을 때, 끝에 Hibernate에서 제공하는 Proxy 객체인 것을 확인할 수 있다.
여기서 한번 더 살펴볼 것은 getId()를 했을 때 select 쿼리가 없이도 값이 조회가 되었다는 것이다. 이는 em.getReference()를 했을 때, 이미 PK 값을 넣어주었기 때문에 프록시 객체가 그 값을 가지고 있는 상황이기 때문에 바로 반환을 해준다.
Member member = new Member();
member.setUserName("memberA");
em.persist(member);
em.flush();
em.clear();
Member reference = em.find(Member.class, member.getId());
System.out.println(reference.getClass());
실제로 find 쿼리를 한번 동일한 구성으로 날려보기로 했다. 이 코드가 실행되었을 때는 select 쿼리가 조회 시점에 실제로 나가야한다. 그리고 실행 결과를 한번 살펴보겠다.
실행 결과를 살펴보면 조회 시점에 select 쿼리가 나간 것을 확인할 수 있고, 저장된 객체의 class 값을 확인하면 프록시 객체가 아닌 것을 확인할 수 있다.
다시 한번 정리하면 find는 find가 실행된 시점에 select 쿼리가 나가 DB에서 실제 엔티티를 가지고 온다. 그렇지만 em.getReference()를 하면 이 메서드 실행 시점에는 select 쿼리가 나가지 않고 프록시 쿼리가 만들어져서 객체에 저장되는 것을 확인할 수 있다.
프록시 객체의 초기화
프록시 객체는 처음에 가져왔을 때는 실제 아무런 값도 가지지 않는 것을 알 수 있다. 그렇다면 프록시 객체는 어떤 순서로 움직일까?
- 프록시 객체는 처음 선언되었을 때는, ID값만 가진 프록시 객체다.
- 실제 엔티티의 값, 메서드가 호출된 시점에 Select 쿼리를 날려 DB에서 엔티티를 가져와 영속성 컨텍스트에 저장한다.
- 프록시 객체는 영속성 컨텍스트에 저장된 객체의 참조를 가리킨다. 그리고 참조를 통해 실제 엔티티의 메서드와 변수에 접근한다.
이 때, 유의해야 할 점은 프록시 객체가 초기화 되었을 때 프록시 객체는 단순히 실제 엔티티의 참조를 가진다는 점이다. 프록시 객체가 실제 엔티티로 대체되는 것이 아니다.
좀 더 쉽게 생각하면 이렇게 볼 수 있을 것 같다. getReference()를 하게 되면 처음에 객체가 가지고 있는 것은 프록시 객체가 조회된다. 그런데 그 객체에서 .getUsername()이라는 메서드를 실행하게 되면, 어~ 나 멤버라는 값이 없는데??라고 하면서 영속성 컨텍스트에 값을 찾아줄 것을 요청한다. 그러면 영속성 컨텍스트는 이 값을 DB에서 조회하는 쿼리를 내보내고, Member Proxy가 그 참조를 가지게 된다.
이 때 Target에 참조가 걸리게 되면, 그 때 부터는 이미 영속성 컨텍스트에 불러져 있기 때문에 추가 Select 쿼리없이 참조에서 바로 불러온다.
프록시 객체의 초기화 코드로 확인
Member member = new Member();
member.setUserName("memberA");
em.persist(member);
em.flush();
em.clear();
Member reference = em.getReference(Member.class, member.getId());
System.out.println(reference.getClass());
System.out.println("reference.getId() = " + reference.getId());
reference.getUserName();
System.out.println(reference.getClass());
위의 코드에서도 reference.getUsername() 메서드를 추가했다. 실제 엔티티의 메서드가 필요한 상황을 추가한 것이다. 이 때, reference에 대한 실제 값이 필요하기 때문에 select 쿼리를 통해 객체를 불러와야 하는 상황이 올 것이다. 실제 코드 실행 내용을 보자
실행 시점에 select 쿼리가 나간 것을 알 수 있다. 즉, 실제 엔티티가 필요한 시점에 select 쿼리가 나가는 것을 확인했다. 그리고 getClass()로 현재 클래스 상태를 확인하면, 초기화 전후로 클래스가 바뀌지 않은 것을 알 수 있다 (Proxy$52oupapW). 앞서 설명한 것처럼 프록시가 초기화 되는 것은 프록시가 필요한 엔티티로 바뀌는 것이 아니라, 프록시가 실제 엔티티의 참조를 가지게 되는 것이기 때문이다.
프록시 객체의 초기화 코드로 확인, 반복 실행
Member member = new Member();
member.setUserName("memberA");
em.persist(member);
em.flush();
em.clear();
Member reference = em.getReference(Member.class, member.getId());
System.out.println(reference.getClass());
System.out.println("reference.getId() = " + reference.getId());
System.out.println("1");
reference.getUserName();
System.out.println("2");
reference.getUserName();
System.out.println("3");
reference.getUserName();
System.out.println("4");
reference.getUserName();
System.out.println("5");
reference.getUserName();
System.out.println(reference.getClass());
위의 코드로 실제 엔티티의 메서드를 여러 번 불렀을 때, 초기화가 여러번 되는지를 확인할 수 있도록 코드를 작성했다. 실행 결과를 아래에서 확인할 수 있다.
위의 실행 결과에서 확인할 수 있듯이, 프록시는 처음 초기화 될 때 단 한번만 select 쿼리를 통해서 DB의 필요한 엔티티를 영속화시킨다. 이 후에는 참조를 통해서 바로 바로 값을 불러오는 것을 확인할 수 있다.
프록시의 특징
프록시의 특징에 대해서 정리하려고 한다. 프록시 특징을 간략히 정리하고, 코드로 하나하나 살펴보려고 한다.
- 프록시 객체는 처음 사용할 때 단 한번만 초기화한다. 한번 초기화되면 그 객체를 참조해서 계속 쓴다.
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화되면 프록시 객체를 통해 엔티티에 접근이 가능해지는 것이다. getClass()로 프록시 객체는 초기화 후에도 프록시 객체인 것을 볼 수 있다.
- 프록시 객체는 원본 엔티티를 상속 받음. 따라서 타입 체크 시, 주의 해야한다. 프록시가 아닌 Member와 프록시 Member는 다른 타입이다. 따라서 == 으로 비교하면 False가 발생하고, InstanceOf로 비교해야 한다.
- 영속성 컨텍스트에 이미 찾는 엔티티가 있으면, em.getReference()를 해도 실제 엔티티를 반환해준다.
- em.getReferecne()로 찾는 엔티티를 불러왔으면 em.find()를 해도 프록시 엔티티를 반환해준다.
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시 객체를 초기화하면 LazyInitiallException이 발생한다.
이 때 주의할 점은 프록시는 JPA의 메인 메커니즘을 반드시 지키려고 한다는 것이다. 예를 들어 a라는 객체가 있다고 하면 어떤 일이 있어도 "a == a"를 비교하게 되면 항상 True가 나오게 하려고 한다. 이 점을 잘 참고해야하는 부분은 4~6번 내용이다.
코드 확인 : 프록시 Member, 그냥 Member equal 비교해보기
Member reference = em.getReference(Member.class, member.getId());
System.out.println("member.getClass() = " + member.getClass());
System.out.println("reference.getClass() = " + reference.getClass());
System.out.println("reference == member : " + (member == reference));
System.out.println("reference == member, class : " + (member.getClass() == reference.getClass()));
System.out.println("reference instanceof member " + (reference instanceof Member));
위의 코드로 == 으로 비교가 가능한지 확인해본다. 객체끼리 같은지 확인해보고, 그리고 각 객체의 클래스끼리 같은지 확인해본다. 그리고 마지막으로 같은 타입인지도 확인해본다.
이 때, 클래스끼리 비교한 것은 다 False가 나오는 것을 확인 할 수 있다.그리고 instanceOf로 확인한 내용은 True인 것을 확인할 수 있다. 이는 당연한 이야기다. 1) 프록시 객체는 참조를 가진 상속받은 객체기 때문에 다른 객체이고 2) 프록시 객체의 클래스는 HibernateProxy로 된 객체다. 따라서 False가 나올 수 밖에 없다.
같은 타입인지 넣기 위해서는 실제로 프록시가 들어올지, 어떤 값이 들어올지 알 수 없기 때문에 instanceOf로 값을 확인하는 것이 적절하다.
코드 확인 : 영속성 컨텍스트에 있을 때, 프록시 객체를 조회한다면
Member member1 = em.find(Member.class, member.getId());
System.out.println("member1= " + member1.getClass());
Member reference = em.getReference(Member.class, member.getId());
System.out.println("reference = " + reference.getClass());
em.find를 통해 필요한 엔티티를 이미 영속화 한 상황에서 getReference를 통해 프록시 객체를 만드는 코드를 작성했다. 이 때는 "a == a"라는 JPA의 큰 메인 메커니즘을 만족하기 위해 reference에는 프록시 객체가 아닌 영속화된 엔티티가 직접 참조된다.
실행 결과는 getClass()의 값인데, 둘다 같은 것을 확인할 수 있다.
코드 확인 : 프록시 객체로 먼저 조회 후, em.find로 찾는다면?
Member reference = em.getReference(Member.class, member.getId());
System.out.println("reference = " + reference.getClass());
Member member1 = em.find(Member.class, member.getId());
System.out.println("member1= " + member1.getClass());
위의 코드를 반대로 실행한 결과다. 동일한 객체를 가리키는데, 프록시가 먼저 선언된 경우다. 이 때도 마찬가지로 a == a를 만족하기 위해서, em.find()가 비록 실제 엔티티를 불러왔다고 하더라도, member1에 선언된 참조는 reference와 마찬가지로 프록시가 된다.
실제 쿼리를 봐도 잘 알 수 있다. 프록시가 먼저 선언된 후에, Select 쿼리까지 나갔으나 실제로 member1에 저장된 객체는 프록시 클래스인 것을 알 수 있다.
준영속 상태일 때, 프록시 객체 초기화 코드 실행
Member member = new Member();
member.setUserName("membera");
em.persist(member);
em.flush();
em.clear();
Member reference = em.getReference(Member.class, member.getId());
System.out.println("reference.getClass() = " + reference.getClass());
em.detach(reference);
System.out.println("reference.getUserName() = " + reference.getTeamId());
위의 코드는 먼저 프록시 객체를 가져와서, 프록시 객체를 영속화 해 둔 상태에서 detach를 통해 준영속화 해버렸다. 그리고 getTeamId()등의 메서드로 강제 초기화를 시켰을 때의 값을 확인했다.
실제 확인했을 때, 프록시 객체까지는 잘 등록되는 것을 확인했다. 그렇지만 준영속화 되고, 초기화 되는 과정에서 제대로 초기화가 되지 못해 LazyInitializationException이 발생하는 것을 확인할 수 있다. 이 에러가 실제 실무에서 아주아주 많이 발생한다고 한다.
프록시와 관련된 편의 기능
1. 프록시 인스턴스의 초기화 여부 확인
emf.getPersistenceUnitUtil().isLoaded(Object)
프록시 객체가 실제로 초기화가 되었는지 위의 코드를 이용해서 확인할 수 있다. emf를 활용해 확인하는 방법이다.
2. 프록시 인스턴스의 클래스 확인
object.getClass();
위 코드로 프록시 인스턴스의 실제 클래스를 확인할 수 있다.
3. 프록시 인스턴스의 강제 초기화
Hibernate.Initialize(Entity)
위에서는 실제 엔티티의 메서드를 이용해서 강제 초기화 하는 방법을 선택했으나, 엄연히 강제 초기화 하는 방법이 있다. 위 코드로도 프록시 인스턴스 초기화가 가능하고, 엔티티 메서드 콜하는 방법으로도 초기화가 가능하다.
프록시의 사용 이유 : 즉시 로딩 / 지연 로딩 기능
앞서 이야기 했던 질문의 답이 나오지 않았다. 그냥 다 불러오면 되지, 굳이 왜 프록시를 사용해야할까? 그 이유는 프록시와 함께 즉시 로딩, 지연 로딩을 함께 사용하게 되면 최적화가 가능하기 때문이다. 예를 들어 아래와 같은 관계가 있다고 해보자.
Member와 Team은 다대일 관계이고, Member에서 Team으로 단방향 연관관계다. 이런 상황일 때, Member의 정보만 필요하다면 굳이 Team 객체까지 불러올 필요가 있을까? 특히 Member가 Team만 가지면 큰 의미는 없을 수 있으나, Member와 연관된 테이블이 100개가 있다고 가정해보자. 그럼 이 때, Member 테이블과 나머지 테이블을 Join + select 쿼리를 날리면 얼마나 느려질까? 프록시는 이런 상황 등에서 최적화를 하는데 도움이 될 수 있다.
지연 로딩과 프록시(FecthType.LAZY)
지연 로딩은 아래에서 Member를 부를 때, Member에 대한 정보만 가져오는 것으로 이해를 하면 될 것 같다. Member를 부르면, Member 객체는 초기화가 되어야 한다. 이 때 Team 객체는 실제 select 쿼리가 나가는 것이 아니라 프록시 객체가 들어가게 되고, 필요한 시점에 로딩한다. 이런 기능을 '지연 로딩'이라고 하고, JPA는 지연로딩을 제공한다.
지연로딩을 이용하기 위해서는 지연로딩의 대상이 될 객체에 어노테이션을 달고, 특정 옵션을 달아주면 된다. 예를 들어 Member와 Team 객체의 관계일 때는 아래처럼 코드를 짤 수 있다. 아래 코드에서 기존 연관관계 설정에서 fetch = fetchType.LAZY 옵션을 켜주면 된다.
@Entity
public class Member {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
FetchType.LAZY 옵션은 지연 로딩을 사용하겠다는 옵션이고, 이 옵션이 붙은 객체는 연관관계를 가진 객체를 초기화 할 때 프록시 객체를 넣어서 초기화한다. 그리고 프록시 객체가 조회 되었을 때, 실제 Select 쿼리로 영속화를 한다.
지연 로딩, 코드로 살펴보기.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUserName("userA");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println(findMember.getClass());
System.out.println(findMember.getTeam().getClass());
System.out.println("findMember.getTeam().getName() = " + findMember.getTeam().getName());
System.out.println(findMember.getTeam().getClass());
예시 코드는 위 코드로 작성했다. 위 코드는 먼저 Team을 DB에 저장하고, Member를 Team에 Join 시켜서 저장하고 영속성 컨텍스트를 다 비운다. 이후 Member 객체를 하나 찾은 후, Team의 Class를 살펴본다. 그 후, Team의 이름을 출력하고, 다시 한번 Team Class를 다시 한번 출력한다.
실제 코드 실행 화면이다. 코드를 실행하면 먼저 Member에 대한 select 쿼리가 나가는 것을 볼 수 있다. 이후, Member와 Team의 클래스를 출력해주는데, Team 클래스는 Proxy 타입인 것을 알 수 있다. 이후 Team의 이름을 출력하라는 코드에서 Team에 대한 Select 쿼리가 나가고, Team의 이름이 정상적으로 출력되는 것을 볼 수 있다.
마지막에 Team 클래스의 타입이 Proxy인 것은 JPA의 원칙 'a==a'를 만족하기 위함이다. 먼저 findMember의 team에 프록시 타입이 들어와있기 때문에 team == team을 만족하기 위해서 클래스는 변하지 않는다. 그리고 team 프록시 객체와 실체 team 객체는 team 프록시 객체의 필드에 참조로 연결된다.
member.getTeam()에서 select 쿼리가 나가는 것은 아니고, member.getTeam().getName()에서 실제 Team의 메서드에 대한 호출이 필요할 때, Select 쿼리가 나가는 것으로 이해하면 된다.
실제 동작은 위처럼 되었다고 볼 수 있다. 단, Team에 색칠이 되어있다고 해서 Team에 진짜 Team 객체가 들어온 것으로 이해를 하면 안된다. Team 프록시 객체와 Team 객체의 참조가 연결된 것으로 보면 된다.
즉시로딩과 프록시
앞에서는 Member와 Team을 함께 조회할 필요가 없을 경우가 더 많을 때를 예로 들었다. 반대로 특정 비지니스 로직에서 Member가 호출되면 Team이 거의 90% 확률로 같이 쓴다면 어떨까? 이 때, 지연로딩으로 select 쿼리가 2번씩 나간다면 그것은 아주 효율성이 떨어진다. 이 때는 Member를 불러올 때, Team을 Join해서 바로 가져오는 것이 효율적이다.
이처럼 Member를 부를 때, Team도 같이 불러오는 것을 즉시로딩이라고 한다. 즉시로딩은 아래 코드처럼 작성하면 된다. fetch 옵션에 FetchType.EAGER를 설정해주면 된다.
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
위처럼 fetch 옵션을 EAGER로 해주면 이 객체는 이 객체를 가진 객체가 초기화 될 때 무조건 같이 초기화된다. 즉, 상위 객체를 find하게 되면, EAGER 설정된 객체는 JOIN SELECT 쿼리로 한번에 불러온다. 즉, 2번의 Select 쿼리가 1번의 select 쿼리가 된다.
실제 동작은 위의 이미지처럼 한다. member1이라는 객체가 불러지게 되면, member1이 속한 팀인 team1 객체도 동시에 Join 쿼리가 나가서 로딩된다.
즉시 로딩 코드로 실습해보기
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUserName("userA");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println(findMember.getClass());
System.out.println(findMember.getTeam().getClass());
System.out.println("findMember.getTeam().getName() = " + findMember.getTeam().getName());
System.out.println(findMember.getTeam().getClass());
지연 로딩을 예시로 알아보던 코드를 동일하게 실행시켜봤다. 즉시로딩은 바로 함께 가져오기 때문에 Select 쿼리가 1회만 나갈 것이고, Team Class를 실제로 가져와보면 모두 프록시가 아니라 실제 Team Class일 것이다.
실행 결과를 살펴보면, 실제로 Member를 찾을 때 한번에 TEAM과 MEMBER를 Join해서 가져오는 것을 볼 수 있다. 그리고 각 클래스 타입도 확인 시, 프록시 타입이 없는 것을 볼 수 있다.
즉시로딩과 지연로딩, 주의 사항
- 가급적 지연 로딩만 사용하는 것이 좋다.
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생함. 예를 들어 Table에 EAGER 10개 걸려있으면, JOIN 10개가 무조건 나가기 때문에 DB에서 조회 속도가 안 나올 수 있음.
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
- @ManyToOne, @OneToOne는 즉시로딩이 기본이다. → LAZY로 설정해야한다.
- @OneToMany, @ManyToMany는 기본이 지연 로딩이다.
즉시로딩으로 설정하면 JPQL에서 N+1 문제가 발생하는 이유는?
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("memberA");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
List members = em.createQuery("select m from Member m").getResultList();
위 코드에서 JPQL 쿼리가 실행되면, N+1이 문제가 발생한다. 먼저 해당 코드를 실행한 결괄르 살펴보면 아래와 같다.
사실은 DB에 있는 Member들만 조회를 하고 싶어서, Select 쿼리를 보냈다. 그런데 즉시로딩으로 설정이 되어있기 때문에 Member안의 Team 객체들도 함께 조회가 된다. 그래서 실제로는 Select 쿼리가 2회가 나가는 것을 볼 수 있다. Member를 1번 조회할 때, 안에 있는 필드 객체 n개만큼 n번의 select 쿼리가 나가기 때문에 N+1 문제라고 한다.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);
Member member = new Member();
member.setUsername("memberA");
member.setTeam(team);
em.persist(member);
Member member1 = new Member();
member1.setUsername("memberB");
member1.setTeam(teamB);
em.persist(member1);
em.flush();
em.clear();
List members = em.createQuery("select m from Member m").getResultList();
최초 쿼리가 1번일 때, 결과 쿼리가 N개만큼 더 생기는 것을 N+1 문제라고 한다. 위 코드에서는 좀 더 자세히 알아볼 수 있다. 각 멤버가 각기 다른 TEAM에 속해있을 때, 전체 멤버를 불러오는 코드다.
이 때, 처음 MEMBER와 연결된 teamA를 가져온다. 그리고 Member1을 살펴보니 얘가 속한 team은 teamB인데 영속성 컨텍스트에 없는 것을 확인했다. 그래서 Select 쿼리가 한번 더 나가는 것을 볼 수 있다. 1번의 쿼리를 쳤는데, 2번의 쿼리가 더 나간 것을 볼 수 있다.
N+1을 해결하는 방법
기본적으로 모든 로딩 방법을 LAZY로 설정한다. 이 다음 분기는 세 가지로 나누어진다.
- JPQL에 fetch Join 기능으로 해결할 수 있다. → 한 방으로 쿼리쳐서 Join되어 Select 딱 한번으로 나온다.
- 엔티티 그래프로 해결
- Batch Size로 해결.
지연 로딩의 활용
아래 내용은 이론적인 것이고, 실무에서는 지연 로딩으로 전부 떡칠해야한다.
- MEMBER / TEAM은 자주 함께 사용한다 → 즉시 로딩
- MEMBER / ORDER는 가끔 함께 사용한다 → 지연 로딩
- ORDER / PRODUCT는 자주 함께 사용 → 즉시 로딩
지연로딩 / 즉시로딩 한 줄 정리
- 이론은 이론일뿐, 모든 연관관계에 지연 로딩을 사용하자.
- 실무에서 즉시 로딩을 사용하지마라.
- N+1 문제는 지연관계를 기본으로 바르고 JPQL fetch 조인, 엔티티 그래프, Bacth Size로 해결해라
- 즉시 로딩을 하게 되면 상상하지도 못한 쿼리가 많이 나간다.
'Spring > JPA' 카테고리의 다른 글
다대일 연관 관계, 외래키는 어디에 있어야할까? (0) | 2021.11.27 |
---|---|
JPA의 연관관계 맵핑 (0) | 2021.11.25 |
영속성 컨텍스트 관련 정리 (0) | 2021.11.20 |
JPA를 활용해 DB에 저장, 조회, 삭제, 수정해보기 (0) | 2021.11.20 |
JPA, Maven으로 Project 생성하기 (1) | 2021.11.20 |