카테고리 없음

자바 ORM 표준 JPA 프로그래밍 - 기본편 섹션 8. 프록시와 연관관계 관리

정현3 2022. 5. 29. 15:40

목차

프록시

즉시로딩과 지연로딩

지연로딩 활용

영속성 전이 : CASCADE

고아 객체

영속성 전이 + 고아객체, 생명주기

 

< 1. 프록시 > 

예를 들어서 Member를 조회할 떄 Team도 DB에서 조회를 해야할까?

'엔티티'를 조회할때 연관된 엔티티들이 항상 사용되는것은 아니다. '연관관계'의 엔티티는 '비즈니스 로직'에 따라 사용될 때도 있지만 그렇지 않을때도 있다

 

       try {

            Member member = new Member();
            member.setUsername("hello");

            em.persist(member);

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

            //Member findMember = em.find(Member.class, member.getId());
            Member findMember = em.getReference(Member.class, member.getId());
            System.out.println("findMember = " + findMember.getClass());
            //Hibername가 만든 가짜 클래스 - 프록시 클래스
            
            System.out.println("findMember.id = " + findMember.getId());
            System.out.println("findMember.Username = " + findMember.getUsername());
            //getUsername을 호출하는 시점에 JPA가 DB에 쿼리를 날린다.

            tx.commit();

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

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

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

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

    }
}

" '지연 로딩 기능'을 사용하려면 실제 엔티티 객체 대상에 '데이터베이스 조회'를 지연할 수 있는 '가짜 객체'가 필요한데 이것을 '프록시 객체'라 한다"

-> Hibernate는 '지연 로딩'을 지원하기 위해 '프록시'를 사용하는 방법을 지원한다.

프록시 - em.getReference() 라는 메서드 : DB에 '쿼리'가 날라가지 않는데 객체가 조회가 된다.

 

->  프록시 객체를 가지고 온다음에 member.getName()을 호출하면 Member의 값이 처음에는 없다

-> MemberProxy 객체에 처음에는 target값이 존재하지 않는다. JPA가 '영속성 컨텍스트'를 통해서 '초기화'를 요청한다. 진짜 Member객체를 가져오게 한다

-> DB에 요청해서 조회해서 진짜 값을 연결해준다

-> '프록시 객체'의 Member target 변수에 '실제 객체'를 연결시켜준다

-> getName()을 하였을때 Member target을 통해서 Member.getName()이 반환된다

-> 프록시 객체에 target이 할당되고 나면, 더이상 '프록시 객체'의 초기화 동작은 없어도 된다.

 

-> "JPA 표준스펙에는 없다. Hibernate와 같은 라이브러리가 구현한다"

 

프록시의 특징

- 프록시 객체는 '처음 사용할 때' 한번만 초기화 ->  초기화한것을 계속 쓴다

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

"프록시는 유지가 되고 내부의 target에만 값이 채워지는것이다"

//Member findMember = em.find(Member.class, member.getId());
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("before findMember = " + findMember.getClass());
System.out.println("findMember.username = " + findMember.getUsername());
System.out.println("after findMember = " + findMember.getClass()); //값이 바뀌지 않는다

tx.commit();

- (심화) 프록시 객체는 '원본 엔티티'를 상속받음, 따라서 '타입 체크'시 주의해야함( 타입비교할때 ==비교 실패, 대신 instance of 사용)

try {

    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.getReference(Member.class, member2.getId());

    System.out.println("m1 == m2" + (m1.getClass() == m2.getClass())); //true
    System.out.println("m1 == m2" + (m1.getClass() == m2.getClass())); //false
    
    tx.commit();
    
    }
private static void logic(Member m1, Member m2) {
    System.out.println("m1 == m2 = " + (m1  instanceof Member));
    System.out.println("m1 == m2 = " + (m2  instanceof Member));
}

- (심화) '영속성 컨텍스트'에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 '실제 엔티티' 반환

 

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

Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getClass()); //Member

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

" JPA에서는 같은 인스턴스(같은 트랜잭션 레벨 = 같은 영속성컨텍스트 안에서) 안에서의 ==비교에 대해서 항상 '같다'라고 나온다"


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

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

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

tx.commit();

->" m1이 실제든 프록시든 상관없이 '== 비교'가 같은 영속성 컨텍스트안(같은 트랜잭션)에서 가져온 것이면 JPA는 항상 true를 반환해주어야만 한다" - JPA가 기본적으로 제공하는 메커니즘이다

- '초기화'는 영속성 컨텍스트의 도움을 받아야만 가능하다.영속성 컨텍스트의 도움을 받을 수 없는 '준영속 상태'일때, 프록시를 초기화 하면 문제가 발생한다.

-> 트랜잭션의 범위 밖에서 '프록시 객체'를 조회하려고 할때

(Hibernate는 org.hibernate.LazyInitializationException 예외를 터트림)

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


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

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

em.detach(refMember); //영속성 컨텍스트를 더이상 관리하지 않는다

refMember.getUsername();

프록시 확인

- 프록시 인스턴스의 '초기화 여부' 확인

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


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

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

    refMember.getUsername(); //초기화
    System.out.println("isLoaded= " + emf.getPersistenceUnitUtil().isLoaded(refMember)); //true

    refMember.getUsername();

PersistenceUnitUtil.isLoaded(Object entity) - 프록시 클래스(인스턴스)의 초기화 여부를 직접 확인

entity.getClass().getName() 출력(..javasist..or HibernateProxy...) - 프록시 클래스 확인 방법

org.hibernate.Hibernate.initialize(entity); - 프록시 강제 초기화

참고 : JPA 표준은 '강제 초기화 없음

강제 호출 : member.getName()

-> '프록시'에 대한 메커니즘을 이해해야 '즉시 로딩'과 '지연 로딩'에 대해 이해할 수 있다.

 

< 2. 즉시 로딩과 지연 로딩 >

'Member 엔티티를 조회할 때 연관된 Team 엔티티도 함께 데이터베이스에서 조회하는것이 좋을까? 아니면 Member엔티티만 조회해두고 Team엔티티는 실제 사용하는 시점에 DB에서 조회하는것이 좋을까?

정답은 없다. 상황에따라 다를 수 있다.

 

즉시 로딩 : 엔티티를 조회할 떄 '연관된 엔티티'도 함께 조회한다

지연 로딩 : 연관된 엔티티를 '실제 사용할 때' 조회한다

지연 로딩 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) //지연 로딩 - 프록시객체 : Member클래스만 DB에서 조회한다
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) //읽기전용매핑, INSERT는 업데이트 X
    private Team team;
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 m = em.find(Member.class, member1.getId()); //쿼리가 멤버만 간다

    System.out.println("m = " + m.getTeam().getClass()); // team은 Proxy로 가져온다

    System.out.println("==============");
    m.getTeam().getName(); //초기화
    System.out.println("==============");

" '조회 대상'이 이미 영속성 컨텍스트 안에 있으면 Proxy객체를 사용할 이유가 없다. 따라서 Proxy가 아닌 '실제 객체'를 사용한다. 예를 들어 team1 엔티티가 영속성 컨텍스트에 이미 로딩되어 있으면 Proxy가 아닌 실제 team1 엔티티를 사용한다."

" 사용하는 시점에 주목 "

-> '쿼리'가 조회를 할때 Member와 Team을 조인을 하고 한방에 땡겨온다. 프록시가 필요없다. 

-> 실제값을 출력해줌

프록시와 '즉시로딩' 주의

-> 가급적 '지연 로딩'만 사용 : 특히 실무에서

-> '즉시 로딩'을 적용하면 예상하지 못한 SQL이 발생

-> '즉시 로딩'은 JPQL에서 N+1을 일으킨다.

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

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

 

'지연 로딩' 활용 - 실무

-> 모든 연관관계에 '지연 로딩'을 사용해라

-> 실무에서 '즉시 로딩'을 사용하지 마라

-> 'JPQL fetch 조인'이나, '엔티티 그래프 기능'을 사용해라

-> '즉시로딩'은 상상하지 못한 '쿼리'가 나간다

 

< 3. 영속성 전이(CASCADE)와 고아 객체 >

@Entity
public class Parent {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) //양방향 연관관계
    private List<Child> childList = new ArrayList<>();
    //Parent를 persist할때 컬렉션까지 다 persist해준다

    public void addChild(Child child) { //연관관계 편의 메서드
        childList.add(child);
        child.setParent(this);
    }
@Entity
public class Child {

    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
try {

 
    Child child1 = new Child();
    Child child2 = new Child();
    
    Parent parent = new Parent();
    child1.setParent(parent); //연관관계 추가
    child2.setParent(parent); //연관관계 추가
    
    parent.addChild(child1);
    parent.addChild(child2);  

    //부모저장, 연관된 자식들 저장
    em.persist(parent);
    
    tx.commit();
    
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
     emf.close();
}

영속성 전이 : CASCADE - 주의

- '영속성 전이'는 '연관관계를 매핑하는것'과 아무 관련이 없음

- 엔티티를 영속화할때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.

CASCADE의 종류

ALL : 모두 적용

PERSIST : 영속 : 엔티티를 영속화 할때, 연관된 엔티티도 함께 유지

REMOVE : 삭제

MERGE : 병합 : 엔티티 상태를 병합할때, 연관된 엔티티도 모두 병함

REFRESH :  상위 엔티티를 '새로고침'할때, 연관된 엔티티를 모두 새로고침

< 4. 고아객체 >

정리

- JPA 구현체들은 '객체 그래프'를 마음껏 탐색할 수 있도록 지원하는데 이때 '프록시 기술'을 사용한다

- 객체를 조회할 때 연관된 객체를 즉시 로딩하는 방법을 '즉시 로딩'이라 하고, 연관된 객체를 지연해서 로딩하는 방법을 '지연 로딩'이라 한다

- 객체를 저장하거나 삭제할 때 '연관된 객체'도 함께 저장하거나 삭제할 수 있는데 이것을 '영속성 전이'라고 한다

- 부모 엔티티가 연관관계가 끊어진 '자식 엔티티'를 자동으로 삭제하려면 '고아 객체 제거 기능'을 사용하면 된다.