[자바 ORM 표준 JPA 프로그래밍] 8장. 프록시와 연관관계 관리
by Jo
8장. 프록시와 연관관계 관리
8.1 프록시
- 엔티티 조회 시 연관된 엔티티가 항상 사용되는 것은 아님
- ∴ JPA는 엔티티가 실제 사용될 때까지 DB 조회를 지연시키는 방법을 제공 ==> 지연 로딩(lazy loading)
- 지연 로딩 기능을 위해 실제 엔티티 객체 대신 DB 조회를 지연시키는 가짜 객체를 사용 ==> 프록시 객체
8.1.1 프록시 기초
- JPA에서
EntityManager.find()
사용하여 엔티티 조회하면 바로 DB에서 조회해 옴 - DB 조회를 엔티티 사용 시점까지 지연시키려면
EntityManager.getReference()
메소드 사용하면 됨- 실제 엔티티 대신 DB 접근을 위임받은 프록시 개체가 반환되어 엔티티 사용시점에 조회할 수 있음
프록시의 특징
- 실제 클래스를 상속받아 만들어져서 실제 클래스와 겉 모습 동일
- 사용하는 입장에서 진짜 객체인지 프록시 개체인지 구분할 필요 X
- 프록시 객체는 실제 객체에 대한 참조(traget) 를 보관
- 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출
- 프록시 구조
- 프록시 위임
프록시 객체의 초기화
- 프록시 객체는
member.getName()
처럼 실제 사용 시 DB 조회해서 실제 엔티티 객체 생성 => 프록시의 초기화
코드
// MemberProxy 반환
Member member = em.getReference(Member.class, "id1");
member.getName(); // 1. getName();
...
class MemberProxy extends Member {
Member target = null; // 실제 엔티티 참조
public String getName() {
if (target == null) {
// 2. 초기화 요청
// 3. DB 조회
// 4. 실제 엔티티 생성 및 참조 보관
this.target = ...;
}
//5. target.getName();
return target.getName();
}
}
- getName(): 프록시 객체에
member.getName()
호출하여 실제 데이터 조회 - 초기화 요청: 실제 엔티티 생성되어 있지 않으면 영속성 컨테스트에 실제 엔티티 생성을 요청
- DB 조회: 영속성 컨텍스트가 DB 조회해서 실제 엔티티 객체 생성
- 실제 엔티티 생성: 프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 멤버 변수에 보관
- target.getName(): 프록시 객체가 실제 엔티티 객체의 getName() 호출하여 결과 반환
프록시의 특징
- 처음 사용할 때 한 번만 초기화 됨
- 프록시 객체 초기화해도 프록시객체가 실제 엔티티로 바뀌지 않음
- 프록시 객체 초기화되면 프록시 객체 통해 실제 엔티티 접근 가능
- 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크시 주의
- 영속성 컨텍스트에 찾는 엔티티 이미 있을 경우 DB 조회 X
em.getReference()
호출시에도 프록시가 아닌 실제 엔티티를 반환- 초기화는 영속성 컨텍스트의 도움 받아야 함
- ∴ 준영속 상태 프록시 초기화시 문제 발생(ex. 하이버네이트에서는
org.higbernate.LazyInitialzationException
)
- ∴ 준영속 상태 프록시 초기화시 문제 발생(ex. 하이버네이트에서는
8.1.2 프록시와 식별자
- 엔티티를 프록시로 조회 시 식별자 값을 파라미터로 전달하는데(ex.
em.getReverenmce(Entity.class, "pk1")
) 프록시 객체는 이 값을 보관 - 엔티티 접근 방식을 프로퍼티(
@Access(AccessType.PROPERTY)
) 로 설정한 경우 식별자 값 조회하는 메소드(ex.Entity.getId()
) 호출해도 프록시 초기화 X - 엔티티 접근 방식을 필드(
@Access(AccessType.FIELD)
) 로 설정 시 식별자 값 조회하는 메소드 호출시에도 프록시 객체를 초기화 함 - 연관관계르 설정할 때는 식별자 값만 사용하기 때문에 프록시를 사용하면 DB 접근 횟수 줄일 수 있음
- ∴ 연관관계 설정 시 프록시 유용하게 사용
- 연관관계 설정할 때는 엔티티 접근망식 필드여도 프록시 초기화 안함
8.1.3 프록시 확인
- JPA가 제공하는 ```PersistenceUnitUtil.isLoaded(Object entity) 메소드 사용시 프록시 인스턴스의 초기화 여부 확인 가능
- 아직 초기화 안되었으면 false
- 초기화 완료되었거나 프록시 인스턴스 아니면 true
- 조회한 엔티티가 진짜 엔티티인지 프록시로 조회한 것인지 확인하려면 클래스 명 충력해보면 됨
- 클래스 명 뒤에 javassist 라 되어있으면 프록시임
- 이건 프록시 생성 라이브러리에 따라 좀 달라질 수 있음
프록시 강제 초기화
하이버네이트의initialize()
메소드로 프록시 강제 초기화 가능
org.hibernate.Hibernate.initilaize(order.getMember()); // 프록시 초기화
JPA 표준에는 프록시 강제 초기화 메소드 없으므로 프록시 메소드 직접 호출해서 초기화하도록 해야 함
8.2 즉시 로딩과 지연 로딩
- 즉시 로딩(eager loading): 엔티티 조회시 연관된 엔티티도 함께 조회
@ManyToOne(fetch = FetcyType.EAGER)
- 지연 로딩(lazy loading): 연관된 엔티티를 실제 사용할 때 조회
@ManyToOne(fetch = FetcyType.LAZY)
8.2.1 즉시 로딩
코드
@Entity
public class Member {
...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
...
}
...
public void find() {
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
}
sql
# em.find(Member.class, "member1");
SELECT
m.member_id AS member_id,
m.team_id AS team_id,
m.username AS username,
t.team_id As team_id,
t.name AS name,
FROM
member m
LEFT OUTER JOIN team t
ON m.team_id = team_id
WHERE
m.member_id = "member1";
- 즉시 로딩 사용시 엔티티 조회할 때 연관 엔티티도 함께 조회
- 대부분의 JPA 구현체에서는 가능하면 조인 쿼리를 사용하여 즉시 로딩함
NULL 제약 조건과 JPA 조인 전략
위 예제의 경우 즉시 로딩 쿼리에서 LEFT OUTER JOIN 을 사용함
=> fk가 nullable 하기 때문에 외부 조인을 한 것임
내부 조인 사용하게 하려면 DB와 Entity 모두 not null 설정 해줘야 함
@JoinColumn(nullalbe = true); // nullable, 외부 조인 사용 (default)
@JoinColumn(nullalbe = false); // not null, 내부 조인 사용
또는@ManyToOne(optional = false)
설정해도 됨
8.2.2 지연 로딩
코드
@Entity
public class Member {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
...
}
...
public void find() {
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
team.getName(); // 팀 객체 실제 사용
}
sql
# em.find(Member.class, "member1");
SELECT *
FROM member
WHERE member_id = "member1";
...
# team.getName();
SELECT *
FROM team
WHERE team_id = "team1";
- 지연 로딩 설정시 ```em.find(Member.class, “member1”) 호출하면 멤버만 조회되고 팀은 조회 X
- team 에는 프록시 객체 넣고 이후
team.getName()
처럼 팀 사용할 때 실제로 조회함 - 만약 대상이 이미 영속성 컨텍스트에 있으면 프록시가 아닌 실제 객체를 사용
8.3 지연 로딩 활용 (예제 분석)
- Member는 Team 하나에만 소속 (N:1)
- 자주 함께 쓰여서 즉시 로딩
- Member는 여러 Order 가짐 (1:N)
- 가끔 사용되어서 지연 로딩
- Order는 Product 가짐 (N:1)
- 자주 함께 쓰여서 즉시 로딩
코드
@Entity
public class Member {
@Id
private String id;
private String username;
private Integer age;
@ManyToOne(fetch = FetchType.EAGER)
private Team team;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Order> orders;
...
}
sql
SELECT
member.id AS memberid,
member.age AS age,
member.team_id as team_id
member.username AS username,
team.id AS teamid,
team.name as name
FROM
member member
LEFT OUTER JOIN team team
ON member.team_id = team.team_id
WHERE
member.id = "member1";
- 회원 엔티티 조회시 위 그림과 같이
- 회원 -> 팀은 실제 엔티티
- 회원 -> 오더 리스트는 프록시로 조회됨
- ∴ 쿼리에서도 order 관련한 내용은 나타나지 않음
8.3.1 프록시와 컬렉션 래퍼
코드
Member member = em.find(Member.class, "member1");
List<Order> orders = member.getOrders();
System.out.println("orders = " + orders.getClass().getName());
// result: orders = org.hibernate.collection.internal.PersistentBag
Order order1 = orders.get(0); // 실제 DB 조회 및 초기화 이뤄짐
- 하이버네이트는 엔티티를 영속상태 만들 때 엔티티에 컬렉션 있으면 이걸 하이버네이트 내장 컬렉션으로 변경함 => 컬렉션 래퍼
- 컬렉션 추적 및 관리 목적
org.hibernate.collection.internal.PersistentBag
- 엔티티 지연 로딩시 프록시 객체로 지연 로딩 수행하듯 컬렉션은 컬렉션 래퍼가 지연 로딩 처리
- 컬렉션 래퍼가 컬렉션에 대한 프록시 역할을 함
- 위 예제에서
member.getOrders()
를 호출해도 컬렉션은 초기화 되지 않고 DB 조회도 되지 않음member.getOrders().get(0)
처럼 실제 데이터 조회해야 DB 조회해서 초기화함
Order order1 = orders.get(0)
로 지연상태인 주문 내역 초기화하면 연관된 Product는 즉시 로딩이므로 함께 조회됨
8.3.2 JPA default fetch 전략
@ManyToOne
,@OneToOne
: 즉시 로딩@OneToMany
,@ManyToMany
: 지연 로딩- JPA는 기본적으로 연관된 엔티티가 하나면 즉시 로딩, 컬렉션이면 지연 로딩을 사용
- 컬렉션은 로딩 비용이 많이 들고 자칫 대량의 데이터를 로딩하게 될 수도 있기 때문
- 기왕이면 전부 지연 로딩 쓰고 나중에 최적화 하면서 필요한 곳만 즉시 로딩 사용하도록 하는게 좋음
8.3.3 컬렉션에 FetchType.EAGER 사용시 주의점
- 컬렉션 하나 이상 즉시 로딩하는 것은 권장 X
- 컬렉션과의 조인은 1:N 조인이기 때문에 조회되는 데이터 너무 많아져서 성능 저하 발생할 수 있음
- 컬렉션 즉시 로딩은 항상 OUTER JOIN 사용됨
- 단건이면 not null 제약조건 걸면 되지만 컬렉션의 경우엔 어떻게 처리할 수가 없기 때문에 항상 외부 조인이 사용됨
FetchType.EAGER 설정 및 조인 전략
@ManyToOne
,@OneToOne
- (optional = false): 내부 조인
- (optional = true): 외부 조인
@OneToMany
,@ManyToMany
- (optional = false): 외부 조인
- (optional = true): 외부 조인
8.4 영속성 전이: CASCADE
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들려면 영속성 전이(transitive persistence) 기능 사용
- JPA는 CASCADE 옵션으로 영속성 전이 제공
코드
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent")
private List<Child> children = new ArrayList<Child>();
...
}
...
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
@ManyToOne
private Parent parent;
...
}
...
// parent 하나에 child 두개 저장하는 경우
private static void saveNoCascade(EntityManager em) {
// parent 저장
Parent parent = new Parent();
em.persist(parent);
// child1 저장
Child child1 = new Child();
child1.setParent(parent); // child -> parent 연관관계 설정
parent.getChildren().add(child1); // parent -> child 연관관계 설정
em.persist(Child1);
// child2 저장
Child child2 = new Child();
child2.setParent(parent); // child -> parent 연관관계 설정
parent.getChildren().add(child2); // parent -> child 연관관계 설정
em.persist(Child2);
}
- JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 함
- ∴ 위 예제에서 parent 를 영속 상태로 만든 이후 추가한 child 엔티티들도 각각 영속상태로 만들어줌
- 영속성 전이를 사용하면 parent만 영속 상태로 만들어도 연관된 child 엔티티들도 한번에 영속 상태로 만들 수 있음
8.4.1 영속성 전이: 저장
코드
@Entity
public class Parent {
...
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<Child>();
...
}
...
private static void saveWithCascade(EntityManager em) {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
child1.setParent(parent); // child1 연관관계 추가
child2.setParent(parent); // child2 연관관계 추가
parent.getChildren().add(child1);
parent.getChildren().add(child2);
// parent 저장, 연관된 child 들도 같이 저장
em.persist(parent);
}
- parent에 child 엔티티들을
@OneToMany(cascade = CascadeType.PERSIST)
로 영속성 전이 설정해줘서 parent 영속화시 child들도 같이 영속화해서 저장됨 - 영속성 전이는 연관 관계 매핑과는 관련 X
- 엔티티 영속화 시 연관된 엔티티도 함께 영속화되는 편리함을 제공할 뿐
8.4.2 영속성 전이: 삭제
코드
@Entity
public class Parent {
...
@OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
private List<Child> children = new ArrayList<Child>();
...
}
...
private static void removeNoCascade(EntityManager em) {
Parent parent = em.find(Parent.class, 1L);
Child child1 = em.find(Child.class, 1L);
Child child2 = em.find(Child.class, 2L);
em.remove(child1);
em.remove(child2);
em.remove(parent);
}
...
private static void removeWithCascade(EntityManager em) {
Parent parent = em.find(Parent.class, 1L);
em.remove(parent);
}
- default로는 삭제 시 위 예시의
removeNoCascade()
처럼 각각 엔티티를 하나씩 제거해야 함 @OneToManyu(cascade = CascadeType.REMOVE)
로 삭제에 대한 영속성 전이 설정해 주면removeWithCascade()
처럼 parent 만 지워도 연관된 child 엔티티들 함께 삭제됨- 이 때 알아서 fk 제약조건 고려하여 child 먼저 삭제하고 parent 삭제해줌
8.4.3 CASCADE의 종류
코드
public enum CascadeType {
ALL // 모두 적용
, PERSIST // 영속
, MERGE // 병합
, REMOVE // 삭제
, REFRESH // refresh
, DETACH // detach
}
- 위 코드에 나와있는 다양한 CascadeType 을 적용하여 상황에 따라 사용 가능
cascade = (CascadeType.PERSIST, CascadeType.REMOVE)
처럼 여러 옵션 같이 적용할 수도 있음CascadeType.PERSIST
,CascadeType.REMOVE
는 각각em.persist()
,em.remove()
실행할 때 바로 전이 발생 X- flush 호출 시 전이 발생함
8.5 고아 객체
- JPA에서는 parent 엔티티와 연관관계 끊어진 child 엔티티 자동 삭제 기능을 제공 => 고아 객체 제거(ORPHAN REMOVAL)
코드
@Entity
public class Parent {
@ID
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<Child>();
...
}
...
public void removeChild(EntityManager em) {
Parent parent = em.find(Parent.class, 1L);
parent.getChildren().remove(0); child 엔티티를 컬렉션에서 제거
}
...
public void removeAllChild(EntityManager em) {
Parent parent = em.find(Parent.class, 1L);
parent.getChildren().clear(); child 엔티티 전부를 컬렉션에서 제거
}
@OneToMany(orphanRemoval = true)
로 설정해주면 연관관계 끊어질 때 child 엔티티도 자동으로 제거됨- 고아 객체 제거 기능도 영속성 컨텍스트 flush 시 적용되므로 flush 시점에 DELETE 쿼리가 수행되어 제거
- 고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이므로 참조하는 곳이 하나일 때만 사용해야 함
- 삭제한 엔티티를 다른 곳에서도 참조중이면 문제 발생할 수 있음
- ∴ orphanRemoval은
@OneToOne
,@OneToMany
에서만 사용 가능
- 또한 parent를 제거해버리면 child도 고아가 되는거나 마찬가지라 parent 제거시 child도 같이 제거됨
CascadeType.REMOVE
설정한 것과 동일함
8.6 영속성 전이 + 고아 객체, 생명주기
- 일반적으로 엔티티는
EntityManager::persist()
를 통해 영속화,EntityManager::remove()
를 통해 제거됨- 엔티티 스스로 생명주기를 관리한다는 의미
- 만약
CascadeType.ALL
과oprhanRemoval = true
를 통시에 사용할 경우- parent 엔티티를 통해 child의 생명주기 관리 가능
- child 저장하려면 parent에 등록만 하면 됨(CASCADE)
- child 삭제하려면 parent에서 제거만 하면 됨(orphanRemoval)
코드
public void save(EntityManager em, parentId, child) {
// parent 엔티티에 등록하여 child도 저장
Parent parent = em.find(Parent.class, parentId);
parent.addChild(Child);
}
public void remove(EntityManager em, parentId, child) {
// parent에서 제거하여 child도 삭제
Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(child);
}
Subscribe via RSS