Spring ORM JPA 관련 정리 - 5

BYEONGMIN CHOI·2022년 9월 22일
0

SPRING ORM JPA

목록 보기
5/5

프록시와 연관관계 관리

프록시(PROXY)

예) Member를 조회할 때 Team도 함께 조회 해야할까?

회원과 팀 함께 출력

 private static void printMember(Member member) {
    String username = member.getUsername();
    System.out.println("username : " + username);
 }

회원만 출력

private static void printMemberAndTeam(Member member) {
    String username = member.getUsername();
    System.out.println("username : " + username);

    Team team = member.getTeam();
    System.out.println("team : " + team.getName());
}

회원만 출력하는 경우 em.find(Member.class, member) 할 경우 member, team 모두 조회하기 때문에 비효율적이다.

프록시 기초

  • em.find() VS em.getReference()

  • em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회 (query 가 진행)

  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회

프록시 특징

  • 실제 클래스를 상속 받아서 만들어진다.
  • 실제 클래스와 겉 모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용(이론상)
  • 프록시 객체는 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

프록시 객체의 초기화

Member findMember = em.getReference(Member.class, member.getId());
findMember.getName()

  • 프록시에 값이 없을 때 영속성 컨텍스트에 초기화 요청을 한다.(DB를 통해 진짜 값을 가져오는 과정)
  • target에 한번 저장되면 계속해서 사용 할 수 있다.

프록시의 특징

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

  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면서 프록시 객체를 통해서 실제 엔티티에 접근 가능한 것이다.

  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야한다.( == 비교 X, 대신 instance of 사용 )

Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId()); // proxy type
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass()));

-> 출력 
m1 == m2 : false
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1 instance of Member));
System.out.println("m1 == m2 : " + (m2 instance of Member));

-> 출력 
m1 == m2 : true
m1 == m2 : true
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면, em.getReference()를 호출해도 실제 엔티티를 반환
    - 한 영속성 컨텍스트 안에 있을 경우 프록시를 반환하는게 아니라 엔티티를 반환한다.
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = " + m1.getClass());

Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass());

System.out.println("m1 == reference : " + (m1 == reference));

-> 출력
m1 = class hellojpa.Member
reference = class hellojpa.Member
m1 == reference : true
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
    (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass()); // Proxy

em.detach(refMember); // detach : 영속성 컨텍스트에서 특정 엔티티를 빼는 것
refMember.getUsername();

-> 출력 결과

프록시 확인

즉시 로딩과 지연 로딩

Member를 조회할 때 Team도 함께 조회해야 할까?

단순히 member 정보만 사용하는 비지니스 로직 println(menber.getName())

지연 로딩 LAZY을 사용해서 프록시로 조회

@Entity
public class Member extends BaseEntity {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    ... Getter, Setter
// Test
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {

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

            Member member1 = new Member();
            member1.setUsername("member1");
            member1.setTeam(team);

            em.persist(member1);

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

            //
            Member findMember = em.find(Member.class, member1.getId());

            System.out.println("findMember = " + findMember.getTeam().getClass());

            // proxy 객체이므로 team의 getName을 할 경우 query를 날림
            System.out.println("======================================");
            //findMember.getTeam() // 이 경우는 proxy값을 가져온다.
            findMember.getTeam().getName(); // 실제 team을 사용하는 시점에 초기화(DB조회)
            System.out.println("======================================");


            tx.commit();
        } catch (Exception e) {
            e.printStackTrace();
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

- 출력 결과

Member와 Team을 자주 함께 사용한다면?

즉시 로딩 EAGER를 사용해서 함께 조회

@Entity
public class Member extends BaseEntity {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    ... Getter, Setter

테스트 코드는 위와 같다.

- 출력 결과

프록시와 즉시로딩 주의

- 가급적 지연 로딩만 사용(특히 실무에서)

- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생 (Join 테이블이 많은 경우 모든 Join 테이블이 query로 조회되기 때문에 성능에 큰 문제가 생긴다.)

- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. ( 1번째 query에 의해 N번의 query가 발생하여 N+1 문제라고 한다.)

List<Member> members = em.createQuery("select m from Member m", Member.class)
                            .getResultList();

// SQL : select * from Member
// SQL : select * from Team where Team_ID = xxx

  • JPQL 로 find 할 경우 SQL 문으로 query 발생 -> Member를 찾고 EAGER 조건에 의해 Member 안에 Team 을 즉시 query 발생 -> 두번의 query가 발생 ( 성능 저하 야기 )

- @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정

- @OneToMany, @ManyToMany는 기본이 지연 로딩

지연 로딩 활용

이론적

  • Member 와 Team은 자주 함께 사용 -> 즉시 로딩
  • Member 와 Team은 자주 가끔 사용 -> 지연 로딩
  • Order와 Product는 자주 함께 사용 -> 즉시 로딩

실무

  • 모든 연관관계에 지연 로딩을 사용해라!
  • 실무에서 즉시 로딩을 사용하지 마라!
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라! (뒤에서 설명)
  • 즉시 로딩은 상상하지 못한 쿼리가 나간다.
profile
스스로 성장하는 개발자가 되겠습니다.

0개의 댓글