[JPA] proxy, fetch 전략

들어가며

프록시의 기본과, JPA에서 fetch 전략이 어떤 것들이 있는 지 알아보자.

프록시 기초

PA에서 식별자로 엔티티 하나를 조회할 때 EntityManager.find()를 사용한다. 이 메서드는 영속성 컨텍스트에 엔티티가 없으면 데이터 베이스를 조회한다.

Member member = em.find(Member.class, "member1");

만약에 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶으면 EntityManger.getReference()를 사용한다.

Member member = em.getReference(Member.class, "member1");

이 메소드를 호출할 때 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다. 대신에 데이터베이스 접근을 위임한 프록시 객체를 반환한다.

즉시로딩 시, JPA는 외부조인(Left Outer) 조인을 사용한다. 그 이유는 내부조인을 사용하게 되면, 외래키 null값이 존재하는 경우, 우리가 원하는 어떤 데이터도 조회할 수 없기 때문이다. 성능적인 측면에서 내부 조인이 좋기 때문에. 내부조인을 사용할려면, 외래키를 not null 조건으로 특정해야 한다

@Entity
public class Member {
  //...
  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name="TEAM_ID", nullable = false)
  private Team team;
  //...
}

2. 예제코드

위의 말이 정확히 이해가 가지 않아서 테스트 케이스를 작성해 보자.
간단한 Team(1): Member(N) 관계를 갖고 (양방향 관계) 매핑을 해보자.

@Data
@Entity
@Table(name = "TEAM")
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    @Column(name = "NAME")
    private String name;

    @OneToMany
    private List<Member> memberList = new ArrayList<>();
}
@Data
@Entity
@Table(name = "MEMBER")
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "NAME")
    private String name;

    @Column(name = "AGE")
    private Integer age;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}
    @Test
    public void teamTest() {

        //Given
        Member member = new Member();
        member.setName("andrew");
        member.setAge(32);

        Team team = new Team();
        team.setName("A팀");

        //연관관계 설정
        member.setTeam(team);
        team.getMemberList().add(member);

        teamRepository.save(team);
        memberRepository.save(member);

        //When
        Member existMember = memberRepository.findById(member.getId()).get();

        //Then
        Assert.assertEquals(existMember.getTeam().getName(), "A팀");

    }

이 테스트는 Member, Team 객체를 저장 하고, 객체 그래프 탐색하는 테스트 케이스 입니다.

위에서 언급했듯이, 즉시 로딩한 경우, left outer join 을 한다.

select
        member0_.member_id as member_i1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_,
        member0_.team_id as team_id4_1_0_,
        team1_.team_id as team_id1_2_1_,
        team1_.name as name2_2_1_
    from
        member member0_
    left outer join
        team team1_
            on member0_.team_id=team1_.team_id
    where
        member0_.member_id=?

2.1. JPA 에서 기본 Fetch 전략

즉시 로딩인지 아닌지 알아 보기 위해서 fetch 옵션을 살펴보자. JPA에서는 기본 fetch 전략은 다음과 같다.

  • @OneToMany // fetch = FetchType.LAZY
  • @ManyToOne // fetch = FetchType.EAGER

연관 관계가 1:N 으로 매핑이 되면, 당연히 즉시 로딩하게 되면, 매번 조인 쿼리가 발생하기 때문에 성능에 안 좋은 영향을 미친다. 그와는 반대로 N:1 은 즉시 로딩이 기본 전략이다.

위의 예제에서는 Member(N): Team(1) 이기 때문에 즉시 로딩이 된다.

이번에는 lazy 로딩으로 테스트 해보자

    @Test
    public void lazy로딩_teamTest() {

        //Given
        Member member = new Member();
        member.setName("andrew");
        member.setAge(32);

        Team team = new Team();
        team.setName("A팀");

        //연관관계 설정
        member.setTeam(team);
        team.getMemberList().add(member);

        teamRepository.save(team);
        memberRepository.save(member);

        //When
        Team existTeam = teamRepository.findById(team.getId()).get();

        //Then
        Assert.assertEquals(existTeam.getMemberList().get(0).getName(), "andrew");

    }

다음과 같은 Exception이 발생한다.

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.demo.spring.jpa.team.Team.memberList, could not initialize proxy - no Session

해결방법은

  • fetchType을 earger로 변경
  • join fetch

두 개가 같은 쿼리를 만드나..?

📚 Related Posts

[JPA] proxy, fetch 전략
Older post

[JPA] 도메인 클래스 컨버터란?

Newer post

SpringBoot, JPA, H2를 이용한 간단한API 작성

[JPA] proxy, fetch 전략