[자바 ORM 표준 JPA 프로그래밍] 8장_프록시와 연관관계 정리

다음 내용들을 정리한다.

  1. 프록시
  2. 즉시로딩, 지연 로딩
  3. 영속성 전이
  4. 고아 객체

프록시

아래 내용을 보기 전에, 우선 프록시에 대한 개념을 파악해야한다.
여기를 참고하자 : https://junhee-ko.github.io/2021/04/17/proxy-pattern/

이제, 예제를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
public class Member {
private String username;
@ManyToOne
private Team team;

// ...
}

@Entity
public class Team {
private String name;

// ...
}

public String printUserBy(String memberId){
Member member = em.find(Member.class, memberId);
System.out.println(member.getUsername());
}

printUserBy 메서드에서, 멤버를 조회할 때 연관된 팀 엔티티까지 DB 에서 같 조회하는게 효율적일까 ?
팀 엔티티를 실제 사용하는 시점에 조회하는게 효율적이다. 이것을 지연로딩이라고 한다.

엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶으면 EntityManager.getReference() 를 사용하면 된다. 이 메서드는, 실제 엔티티 객체를 생성하지 않고 프록시 객체를 반환한다.

프록시 특징

프록시 객체는 실제 클래스를 상속 받아 만들어지며, 실제 객체에 대한 참조(target) 을 보관한다. 프록시 객체의 메소드를 호출하면, 프록시 객체는 실제 객체의 메소드를 호출한다.

프록시 객체의 초기화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Member member = em.getReference(Member.class, "id1");
member.getUsername(); // 실제 사용 (1)

public class MemberProxy extends Member {
Member target = null;

public String getUsername() {
if(target == null){
// 초기화 (2)
// DB 조회 (3)
// 실제 엔티티 생성 및 참조 보관 (4)
}

return target.getUsername(); // (5)
}
}

프록시 객체의 초기화란, member.getUsername() 처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티를 생성하는 것이다. 다음 과정과 같다.

  1. 프록시 객체에 member.getUsername() 호출해서 실제 데이터 조회
  2. 프로시 객체는 실제 엔티티가 생성되어 있지 않으면, 영속성 컨텍스트에 실제 엔티티 생성 요청 (== 초기화 요청)
  3. 영속성 컨텍스트는 DB 조회해서 실제 엔티티 객체 생성
  4. 프록시 객체는 실제 엔티티 객체의 참조를 Member target 참조 변수에 보관
  5. 프록시 객체는 실제 엔티티 객체의 getUsername() 을 호출해서 결과 반환

주의할 점은,

  1. 영속성 컨텍스트에 이미 찾는 엔티티가 있으면, DB 를 조회할 필요가 없으므로 em.getReference() 를 호출해도 프록시가 아니라 실제 엔티티를 반환
  2. 초기화는 영속성 컨텍스트의 도움을 받아야 가능. 그래서, 준영속 상태의 프록시를 초기화하면 문제가 발생 (Hibernate : LazyInitializationException 발생)

즉시 로딩

1
2
3
4
5
6
7
8
9
10
@Entity
public class Member {
// ...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
// ...
}

em.find(Member.class, "member1")

멤버를 조회하는 순간 팀도 함께 조회한다. 여기서 주의할 점은 회원과 팀 두 테이블을 조회해야하므로 쿼리를 두 번 실행하는 것이 아니라, join query 를 사용한다는 것이다.

지연 로딩

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
public class Member {
// ...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// ...
}

Member member = em.find(Member.class, "member1"); // 회원만 조회
Team team = member.getTeam(); // 여기서 team 은 프록시 객체
team.getName(); // team 객체 실제 사용하면서 프록시 객체가 초기화됨

만약, team 엔티티가 영속성 컨텍스트에 이미 로딩되어 있으면, 프록시가 아닌 실제 엔티티를 사용한다.

JPA 기본 Fetch 전략

  1. @ManyToOne, @OneToOne : FetchType.EAGER
  2. @OneToMany, @ManyToMany : FetchType.LAZY

연관된 엔티티가 하나면 즉시 로딩을, 컬렉션이면 지연로딩을 사용한다. 컬렉션을 로딩하는 것은 비용이 많이 들고 자칫하면 너무 많은 데이터를 로드할 수 있기 때문이다.
추천하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것이다. 그리고, 실제 사용하는 상황을 보고 꼭 필요한 곳에 즉시 로딩을 사용하도록 최적화 하면 된다.

영속성 전이

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용하면 된다. JPA 는 CASCADE 옵션으로 영속성 전이 기능을 제공힌디.
예를 들어, 부모 엔티티가 여러 자식 엔티티를 가지고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany
private List<Child> children = new ArrayList<Child>();
}

@Entity
public class Child {
@Id @GeneratedValue
private Long id;
@ManyToOne
private Parent parent;
}

영속성 전이 : 저장

부모만 영속화하면 CascadeType.PERSIST 로 설정한 자식 엔티티까지 함께 영속화해서 저장한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<Child>();
}

private static void saveWithCascade(EntityManager em){
Child child01 = new Child();
Child child02 = new Child();
Parent parent = new Parent();

// 연관 관계 추가
child01.setParent(parent);
child02.setParent(parent);
parent.getChildren().add(child01);
parent.getChildren().add(child02);

// 부모를 저장하며, 연관된 자식들도 같이 저장
em.persist(parent);
}

영속성 전이 : 삭제

CascadeType.REMOVE 설정하고 부모 엔티티만 삭제하면 연관된 자식 엔티티까지 함께 삭제된다.
현재 예시로는, DELETE SQL 이 세 번 실행된다. (부모 + 자식01 + 자식02)

1
2
Parent parent = em.find(Parent.class, 1L);
em.remove(parent);

영속성 전이 발생 시점

CascadeType.PERSIST 와 CascadeType.REMOVE 는 em.persist() 와 em.remove() 를 실행할 때 바로 전이가 발생하는 것이 아니라, 플러쉬를 호출할 때 발생한다.

고아 객체

JPA는 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데, 이를 고아 객체 제거라고 한다. 아래 코드를 보자.

1
2
3
4
5
6
7
8
9
10
@Entity
public class Parent {
// ...
@OneToMany(mappedBy="parent", orphanRemoval = true)
private List<Child> children = new ArrayList<Child>();
// ...
}

Parent parent = em.find(Parent.class, id);
parent.getChildren.remove(0); // 자식 엔티티를 컬렉션에서 제거

컬렉션에서 첫 번째 자식을 제거하면, orphanRemoval = true 옵션 설정으로 인해서 데이터베이스의 데이터도 삭제된다.
주의할 점은, 고아 객체 제거 기능은 영속성 컨텍스트를 flush 할 때 적용되므로, flush 시점에 DELETE SQL 이 실행된다.

참고로, 모든 자식 엔티티를 제거하려면 다음과 같이 하면된다.

1
parent.getChildren.clear();

영속성 전이 + 고아객체

일반적으로 엔티티는 em.persist()로 영속화되고, em.remove()로 제거된다. 이것은 엔티티 스스로 생명주기를 관리한다는 뜻이다.
그런데, CascadeType.All + orphanRemove = true 를 동시에 사용하면? 부모 엔티티를 통해서 자식 엔티티의 생명주기를 관리할 수 있다.

자식을 저장하려면, 부모만 등록.

1
2
Parent parent = em.find(Parent.class, id);
parent.addChild(child);

자식을 삭제하려면, 부모만 삭제

1
2
Parent parent = em.find(Parent.class, id);
parent.getChildren().remove(child);

자바 ORM 표준 프로그래밍 <김영한>

Comments