JPA 프록시와 연관관계 - 프록시

2022. 7. 4. 03:16·공부/JPA
반응형

출처 : 자바 ORM 표준 JPA 프로그래밍 인프런 강의

 

JPA 게시글은 대부분 인프런의 김영한님의 강의인 '자바 ORM 표준 JPA 프로그래밍' 기반으로 내용을 정리했습니다.

 

프록시

나는 둘 중에 하나만 조회하고 싶어

Member와 Team이 연관관계로 매핑되어 있다 하자. 그럼 Member만 조회해야 할 때도 있고, Team만 조회해야 할 때도 있고, Member와 Team을 모두 조회해야 할 때도 있다. 

Member와 Team 함께 출력

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());
}

Member만 출력

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

Member와 Team을 함께 출력할 때야 문제가 없지만 Member만 조회하고 싶은데 Team까지 같이 딸려 들어와 버리면 최적화가 잘 되어있지 않은 것이다. 이런 경우엔 최적화할 필요가 있어 JPA에서는 지연 로딩과 프록시를 지원한다.

 

em.getReference()

  • em.find() : 데이터베이스를 통해서 실제 엔티티 객체를 조회한다.
  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회. 즉 DB에는 SQL 쿼리가 나가지 않는데 객체는 조회하는 것이다.

getReference() 사용하기

  • em.find() 사용
//Member.class
@Entity
public class Member  extends BaseEntity{

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

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

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    ...생략
    

// 실행 클래스
...생략
Member member = new Member();
member.setUsername("hello");
em.persist(member);

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

Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
...생략

    
// 출력 로그
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.createdBy as createdB2_3_0_,
        member0_.createdDate as createdD3_3_0_,
        member0_.lastModifiedBy as lastModi4_3_0_,
        member0_.lastModifiedDate as lastModi5_3_0_,
        member0_.TEAM_ID as TEAM_ID7_3_0_,
        member0_.USERNAME as USERNAME6_3_0_,
        team1_.TEAM_ID as TEAM_ID1_5_1_,
        team1_.createdBy as createdB2_5_1_,
        team1_.createdDate as createdD3_5_1_,
        team1_.lastModifiedBy as lastModi4_5_1_,
        team1_.lastModifiedDate as lastModi5_5_1_,
        team1_.name as name6_5_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?

em.find()를 사용하니 Member 객체에 관한 출력만 필요한데도 Team 테이블까지 조인해 SELECT 쿼리를 전송한다.

 

  • em.getReference()
//            Member findMember = em.find(Member.class, member.getId());
            Member findMember = em.getReference(Member.class, member.getId()); //**
//            System.out.println("findMember.id = " + findMember.getId());
//            System.out.println("findMember.username = " + findMember.getUsername());

위의 코드에서 em.find() 코드를 주석 처리하고 대신 getReference()를 사용해서 실행시키면 SELECT SQL이 전송되지 않는다. 그럼 이번엔 찾은 member의 값을 꺼내는 출력문의 주석을 풀어보자.

//Member findMember = em.find(Member.class, member.getId());
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());



// 출력 로그
findMember.id = 1 // id 출력
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.createdBy as createdB2_3_0_,
        member0_.createdDate as createdD3_3_0_,
        member0_.lastModifiedBy as lastModi4_3_0_,
        member0_.lastModifiedDate as lastModi5_3_0_,
        member0_.TEAM_ID as TEAM_ID7_3_0_,
        member0_.USERNAME as USERNAME6_3_0_,
        team1_.TEAM_ID as TEAM_ID1_5_1_,
        team1_.createdBy as createdB2_5_1_,
        team1_.createdDate as createdD3_5_1_,
        team1_.lastModifiedBy as lastModi4_5_1_,
        team1_.lastModifiedDate as lastModi5_5_1_,
        team1_.name as name6_5_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
findMember.username =  // username 출력

그러면 이제 다시 또 Member와 Team을 JOIN 한 SELECT SQL이 실행된다.
em.getReference() 코드를 실행할 때에는 쿼리가 발생하지 않는다. em.getReference()로 찾아온 객체를 사용할 때에 SQL 쿼리가 발생하는데, findMember.getId()의 경우엔 이미 em.getReference()에서 member.getId() 값을 넣어주었으므로 DB까지 다녀오지 않아도 알 수 있는 값이라 이때도 쿼리는 발생하지 않는다. 다만 find.getUsername()은 DB에서 조회를 해야지만 알 수 있는 값이기 때문에 이때 DB에 SQL문이 전달된다.

그럼 저 em.getReference()로 가져온 findMember는 도대체 뭘까? 출력해보자.

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

// 출력 로그
findMember = class hellojpa.Member$HibernateProxy$nizRdaZG

findMember는 하이버네이트에서 만든 가짜 클래스이다.

 

 

프록시의 특징

 

  • 실제 클래스를 상속받아서 만들어진다.
  • 실제 클래스와 겉모양이 같다.
  • 이론상 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.

 

 

 

 

  • 프록시 객체는 실제 객체의 참조(target)를 보관한다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

 

 

프록시 객체의 초기화

Member member = em.getReference(Member.class, 1L);
member.getName();

  1. 클라이언트가 프록시 객체의 getName()을 호출한다.
  2. 프록시 target이 null이면 영속성 컨텍스트에 초기화를 요청한다.
  3. 영속성 컨텍스트에서 DB를 조회한다.
  4. DB에 조회한 데이터로 실제 Entity를 생성한다.
  5. 프록시의 target에 member 엔티티 정보가 저장되고, target.getName()을 했을 때 실제 객체에 존재하는 getName()을 호출한다.

초기화 이후 getName()을 호출하면 DB 조회는 이루어지지 않고 target에 값이 존재하니까 객체에 있는 값을 호출한다.

사실 프록시 객체에 대한 메커니즘은 JPA 표준 스펙에 존재하지 않는다. 하이버네이트 등의 라이브러리가 구현하기 나름이긴 하지만 기본적인 매커니즘은 위와 같다.

 

 

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
  • 프록시 객체를 초기화 할 때, 프록시가 실제 엔티티로 교체되는게 아니라, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능한 것이다. 
  • 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크 시 주의해야 한다. 
    (== 비교 XXXXX, 대신 instance of를 사용해야 한다.)
// em.find() == em.find()
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
em.persist(member2);

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

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

// 출력 로그
m1 == m2 : true

em.find()로 찾은 객체와 em.find()로 찾은 객체는 == 비교 시 true가 반환된다.

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

// 출력 로그
m1 == m2 : false

em.find()로 찾은 객체와 em.getReference()로 찾은 객체는 == 비교 시 false가 반환된다.

System.out.println("m1 == Member : " + (m1 instanceof Member)); //true
System.out.println("m1 == Member : " + (m2 instanceof Member)); //true

타입 비교를 할 때는 instanceof를 사용해야 한다.

 

  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 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());

// 출력 로그
m1 = class hellojpa.Member
reference = class hellojpa.Member

getReference() 임에도 불구하고 프록시 객체가 아니라 Member 엔티티가 찍히는 이유는 두 가지가 있다.

  1. 영속성 컨텍스트에 있는 1차 캐시에 이미 값이 있는데 굳이 DB를 조회해 올 필요가 없다. 즉, 프록시 객체를 반환하는 이유가 없다.
  2. 같은 트랜잭션 안에 있고, 같은 id를 가진 객체는 무조건적으로 JPA에서 == 비교 시 true가 나오게 보장해주는 메커니즘이 존재한다.

2번과 같은 이유로 em.getReference() 후 em.find()를 하면 둘 다 프록시 객체를 반환하는 걸 볼 수 있다.

Member member2 = new Member();
member2.setUsername("member2");
em.persist(member2);

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

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

Member findM2 = em.find(Member.class, member2.getId());
System.out.println("findM2.getClass() = " + findM2.getClass());



// 출력 로그
referenceM2.getClass() = class hellojpa.Member$HibernateProxy$Dl9hL0nE
findM2.getClass() = class hellojpa.Member$HibernateProxy$Dl9hL0nE

 

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.
    하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트린다.
...생략

try {

    Member member1 = new Member();
    member1.setUsername("member1");
    em.persist(member1);


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

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

    em.detach(reference); // reference 객체를 영속성 컨텍스트에서 제외

    reference.getUsername(); // 프록시 초기화

    tx.commit();
} catch (Exception e) {
    tx.rollback();
    e.printStackTrace(); // 에러 로그 출력
} 
... 생략

위의 코드를 실행하면 에러가 터진다.

em.detach() 대신 em.close(), em.clear()를 해도 에러가 발생한다. 실무에서 많이 발생하는 문제이니 주의해서 코딩해야 한다.

 

 

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인
    PersistenceUnitUtil.isLoaded(Object entity)
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass()); // Proxy

System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(reference)); // false


//=============================//


Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass()); // Proxy
reference.getUsername();
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(reference)); // true
  • 프록시 클래스 확인 방법
    entity.getClass().getName() 출력(..javasist...or HibernateProxy...)
System.out.println("reference = " + reference.getClass());
  • 프록시 강제 초기화. JPA 표준에는 강제 초기화가 없고 하이버네이트에서 지원하는 기능이다.
    org.hibernate.Hibernate.initialize(entity)
Hibernate.initialize(reference);

 

반응형
저작자표시 비영리 변경금지 (새창열림)
'공부/JPA' 카테고리의 다른 글
  • JPA 프록시와 연관관계 - 영속성 전이(CASCADE)와 고아 객체
  • JPA 프록시와 연관관계 - 즉시 로딩과 지연 로딩
  • JPA 고급 매핑 - Mapped Superclass 매핑 정보 상속
  • JPA 고급 매핑 - 상속관계 매핑
데부한
데부한
어차피 할 거면 긍정적으로 하고 싶은 개발자
    반응형
  • 데부한
    동동이개발바닥
    데부한
  • 전체
    오늘
    어제
    • 분류 전체보기 (307)
      • 방통대 컴퓨터과학과 (27)
        • 잡담 (9)
        • 3학년1학기 (17)
      • 프로젝트 및 컨퍼런스 회고 (1)
        • 프로젝트 (4)
        • 한이음 프로젝트 (0)
        • 회고 (3)
      • 공부 (165)
        • Spring (37)
        • JPA (71)
        • 인프런 워밍업 클럽_BE (10)
        • Java (6)
        • React.js (27)
        • 넥사크로 (11)
        • 기타 (3)
      • 알고리즘 (85)
        • 알고리즘 유형 (10)
        • 알고리즘 풀이 (57)
        • SQL 풀이 (18)
      • 에러 해결 (13)
      • 잡담 (7)
        • 국비교육 (2)
        • 구매후기 (5)
        • 진짜 잡담 (0)
  • 블로그 메뉴

    • Github
    • Linkedin
    • 홈
    • 방명록
    • 글쓰기
    • 관리
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    react
    코딩테스트
    프론트엔드
    스프링부트
    egov
    백준
    MSA
    Spring
    토이프로젝트
    토비의스프링부트
    Java
    개발자
    oracle
    넥사크로
    SpringBoot를 이용한 RESTful Web Service 개발
    운영체제
    프로그래머스
    자바스크립트
    IT
    RESTful
    기출문제
    전자정부프레임워크
    QueryDSL
    springboot
    JPA
    인프런
    방통대
    SQL
    에러해결
    알고리즘
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
데부한
JPA 프록시와 연관관계 - 프록시
상단으로

티스토리툴바