프록시와 연관관계 관리

식빵·2022년 1월 9일
0

JPA 이론 및 실습

목록 보기
10/17

🍀 미리 알아두기

JPA 표준 명세에서는 지연 로딩의 구현 방법을 JPA 구현체에 위임했다.
그래서 지금부터 설명되는 내용은 모두 하이버네이트가 지연로딩을 구현한 방법에 대한 설명이다.

하이버네이트는 지연로딩을 위해서 프록시 방식, 바이트코드를 수정 방식 2가지로 나뉜다. 바이트코드 수정 방식은 복잡하고, 일반적으로 사람들이 많이 쓰는 프록시 방식으로 설명하겠다.




🍀 Proxy

자바 객체만 써서 연관관계를 갖는 객체들을 탐색해 나가면 당연히 NPE(Null Pointer Exception)이 터질 것이다. 하지만 JPA 에서 제공하는 프록시 덕분에 우리는 엔티티 객체를 사용해서 자유롭게 객체 탐색을 할 수 있다.

참고
객체 탐색은 그냥 연관관계를 갖는 객체를 "." 으로 접근하는 것을 의미한다.

example:

class A {
    B b;
}
class B{ /* not important now ... */}
public static void main () {
    A a = new A();
    a.b; // 요런걸 객체 탐색이라고 생각하면 됩니다.
}




🐛 Proxy 기초개념

프록시는 이미 존재하는 클래스를 상속받아서 만들어진 클래스의 객체이다.
그래서 사용하는 입장에서는 프록시와 실제 객체의 구분을 하지 않고 사용할 수 있다.

그리고 이런 프록시 객체는 주로 실제 객체에 대한 참조를 갖고 있으며,
외부에서 프록시 객체의 메소드를 호출하면 프록시 객체는 다시 실제 객체의 메소드를 호출한다.




🐛 Proxy 객체의 초기화

Hibernate 에서는 프록시 객체의 메소드를 사용하는 시점에 데이터베이스 조회하여,
엔티티 객체를 생성하고, 프록시 객체가 이 객체의 참조값을 갖는다.
이것을 프록시의 초기화라고 한다.




🐛 Proxy 써보기

Proxy를 직접 쓰고 싶으면 EntityManager.getReference(clazz, id) 호출하자.
전에 사용하던 EntityManager.find(clazz, id)는 그 즉시 데이터베이스를 조회하여 엔티티를 반환하지만, getReference 는 원본 엔티티를 참조할 수 있는 프록시 객체를 반환한다.

이런 getReference 는 언제 쓸까?
주로 연관관계를 설정할 때 유용하다.
SQL을 실행하지 않고도, 연관관계를 맺을 수 있는 장점이 있다.

Member member = em.find(Member.class, 1L);
Team team = em.getReference(Team.class, 2L);
member.setTeam(team);

주의!!!!

만약 EntityManager.getReference(Member.class, -1L) 처럼해서 DB에 존재하지 않는 것을 프록시로 갖고 있고, 실제 메소드를 호출하면 에러가 난다.

그렇다면 우리가 em.find를 통해 엔티티를 처음 생성되고 연관관계를 갖는 필드에 프록시를 넣어주는 시점에, DB에서 연관관계를 현재 갖지 않고 있다면 JPA는 필드에 프록시를 넣을까?

아니다. JPA에서는 그런 경우에 null을 주거나, 비어있는 컬렉션 래퍼 인스턴스를 준다.

JPA의 연관관계와 관련된 프록시 주입 방식 2가지를 알아보자.

1. 연관관계 필드 타입이 Collection 인 경우:
PersistentBag 라는 컬렉션 래퍼 클래스 의 인스턴스를 할당한다.
주의할 점은 Collection 필드는 절대로 null이 들어가지 않는다!
연관관계가 없다면 비어있는 컬렉션 래퍼 인스턴스를 필드에 할당한다.
참고로 컬렉션 래퍼는 프록시의 역할도 해준다.

2. 연관관계 필드 타입이 Custom Class 인 경우:
둘 중 하나다. null 또는 Custom Class를 상속한 프록시 클래스의 인스턴스다.
연관관계가 없다면 null, 있다면 프록시를 필드에 할당한다.




🐛 Proxy 특징

  • 프록시 객체는 딱 한번만 초기화
  • 프록시 객체를 초기화되더라도, 프록시 객체 자체가 실제 엔티티로 바뀌는 것이 아님
  • 프록시 객체는 오리지날 엔티티를 상속 받은 것이니 타입 체크 유의
  • 영속성 컨텍스트에 조회하는 엔티티가 이미 있다면 em.getReference()를 호출했을 때, 프록시 대신 그냥 이미 존재하는 엔티티를 반환
  • 프록시 초기화는 영속성 컨텍스트의 도움이 필요




🐛 즉시 로딩과 지연 로딩


1. 특징

- 즉시 로딩

  • Proxy 사용 ❌
  • EntityManager.find 호출 시, join query를 사용하여 연관관계와 관련된 엔티티를 그 즉시 조회
  • JPQL을 사용하면 root가 되는 엔티티를 먼저 찾아낸 후, select 쿼리를 또 실행해서 연관관계 엔티티도 조회
  • 최종적으로 엔티티 인스턴스(또는 하이버네이트 컬렉션 래퍼) 혹은 null을 연관관계 필드에 할당

- 지연 로딩

  • Proxy 사용 👌
  • EntityManager.find 호출 시, 현재 찾고자 하는 엔티티만 select로 조회
  • JPQL을 사용하면 root가 되는 엔티티만 찾아내고 나머진 proxy 처리.
    단, fetch를 사용하면 즉시 로딩으로 전환 가능
  • 최종적으로 Proxy(또는 하이버네이트 컬렉션 래퍼) 혹은 null을 연관관계 필드에 할당

참고: fetch를 사용하지 않는 JPQL 에서 말하는 fetchJPQL은 더 진도를 나가면 알게 된다.


2. 설정 방법

연관관계 필드 위에 있는 다중성 애노테이션 (ex: @ManyToOne, OneToMany 등)의 속성 중에 fetch 에 속성값을 통해서 지정이 가능하다.

아래 예제를 보자.

@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩!
@JoinColumn(name = "team_id")
private Team team;
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩!
@JoinColumn(name = "team_id")
private Team team;

만약 fetch를 지정하지 않으면 default가 적용된다.

  • xxxToMany : FetchType.LAZY
  • xxxToOne : FetchType.EAGER

3. 선택 방법

FetchType.LAZY 를 쓰자.
FetchType.LAZY 를 쓰면 최적화의 기회를 얻을 수 있지만,
FetchType.EAGER 는 그런 최적화의 기회 자체가 없다.




🐛 Proxy 테스트 코드


1. 엔티티 코드


@Entity @Getter @Setter
public class Team {

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

    private String name;
}

@Entity @Getter @Setter
public class Member {

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

    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}


2. 테스트 코드


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

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

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

Member findMember = em.find(Member.class, member.getId());
Team team1 = findMember.getTeam();

System.out.println("=================================1111111");
System.out.println("team1.getId() = " + team1.getId());

System.out.println("=================================1222222");
System.out.println("findMember = " + team1.getName());


3. 콘솔 출력


Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.team_id as team_id3_0_0_,
        member0_.username as username2_0_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?
=================================1111111
team1.getId() = 24
=================================1222222
Hibernate: 
    select
        team0_.team_id as team_id1_2_0_,
        team0_.name as name2_2_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
findMember = team1





🍀 영속성 전이: Cascade

정말 간단한 기능이다.
JPA 에서 어떤 연관관계의 필드를 갖는 엔티티가 persist, delete 등을 할때
갖고 있는 연관관계 필드 엔티티도 동시에 persist, delete 해주는 기능이다.


엔티티 코드

@Entity @Setter @Getter
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) // 이렇게 하면 끝
    private List<Child> children = new ArrayList<>();
}
@Entity @Getter @Setter
public class Child {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    private Parent parent;
}

테스트 코드

Parent parent = new Parent();
em.persist(parent);

Child child1 = new Child();
Child child2 = new Child();
child1.setParent(parent); // 연관관계의 주인에 set을 해야 실제 외래키가 적용된다!
child2.setParent(parent);
parent.getChildren().add(child1);
parent.getChildren().add(child2);

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

Parent findParent = em.find(Parent.class, parent.getId());
em.remove(findParent);




🍀 고아 객체

부모 엔티티의 컬렉션에서 자식 엔티티의 참조를 제거하면 자식 엔티티가 자동으로 삭제되는 기능


엔티티 코드

@Entity @Setter @Getter
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    // orphanRemoval=true!!
    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<>();
}
@Entity @Getter @Setter
public class Child {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    private Parent parent;
}

테스트 코드

// 이미 DB에 Child 데이터가 있어야 한다!
Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent.getChildren().remove(0));

삭제 쿼리 확인

Hibernate: 
    delete 
    from
        child 
    where
        id=?

주의할 점 (중요) :
고아 객체는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다. 그러므로 이 기능은 참조하는 곳이 하나일 때만 사용한다.




🍀 참고

JPA와 Hibernate의 Id 조회와 관련된 차이
자바 ORM 표준 JPA 프로그래밍
인프런 JPA 관련 로드맵

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글