computer_study

[JPA] 08. 프록시와 연관관계 관리 본문

스터디/자바 ORM표준 JPA 프로그래밍

[JPA] 08. 프록시와 연관관계 관리

knowable 2022. 8. 22. 02:12

1. 프록시

지연로딩 : 엔티티를 조회할 때 엔티티를 항상 사용하진 않기에, 엔티티가 실제 사용될 때 까지 데이터베이스 조회를 지연하는 방법

이런 지연 로딩을 위해 조회를 지연할 수 있는 가짜 객체가 필요. 이것이 프록시.

 

1.1 프록시 기초

  • 프록시의 특징
    • 프록시 클래스는 실제 클래스를 상속받아 만들어진다.
    • 사용자 입장은 프록시 객체와 진짜 객체를 구분하지않고 사용할 수 있다.
    • 처음 사용할 떄 한 번만 초기화 된다. (실제 엔티티로 바뀌는 것이 아니라 프록시를 통해 접근할 수 있게 되는 것)
    • 원본 엔티티를 상속받기에 타입 체크 시에 주의해서 사용해야 한다.
    • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 DB접근이 필요 없기에 실제 엔티티를 반환한다.
    • 준영속 상테의 프록시를 초기화하면 문제가 발생한다.
  • 프록시 객체의 초기화
    • 프록시객체가 실제 사용되어 실제 엔티티 객체를 생성하는 것이 프록시 객체의 초기화

1.2 프록시와 식별자

  • 엔티티를 프록시로 조회 시 PK값을 파라미터로 전달. 프록시 객체는 이 식별자 값을 보관한다.
  • 결국 식별자를 조회 시에는 DB접근을 안하기에 프록시를 초기화 하지 않는다.(엔티티 접근 방식을 프로퍼티로 설정한 경우)
  • 엔티티 접근 방식이 필드면 프록시 객체를 초기화한다.
  • 연관관계 설정 시 프록시를 사용하면 DB 접근 횟수를 줄일 수 있기에 이익을 볼 수 있다.

1.3 프록시 확인

  • PersistenceUnitUtil.isLoaded(Object entity)메소드를 통해 프록시 인스턴스의 초기화 여부를 확인할 수 있다.
  • 초기화가 되어있거나 프록시 인스턴스가 아니면 true를 반환
  • 조회한 엔티티가 진짜인지, 프록시로 조회인지 확인을 위해선 클래스 명을 출력해보면 알 수 있다.

 

2. 즉시 로딩과 지연 로딩

2.1 즉시 로딩

  • 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
  • @ManyToOne(fetch = FetchType.EAGER)로 설정
  • ex) member를 호출했다면 그에 연관된 team도 함께 조회
  • 조회 시 쿼리를 두 번 수행하지 않고, 가능하면 조인 쿼리를 사용(즉시 로딩을 최적화하기 위해)
  • 비즈니스 로직에서 항상 같이 조회하는 경우에 즉시로딩을 쓰면 좋다.

2.2 지연 로딩

  • 연관된 엔티티를 실제 사용할 때 조회한다.
  • @ManyToOne(fetch=FetchType.LAZY)로 설정
  • ex) member호출 시 연관된 team은 조회하지 않고 team멤버변수에 프록시 객체를 넣어둔다.
  • 실제 사용이 될 떄 프록시 객체가 데이터 로딩을 수행(초기화)
  • 필요할 떄 마다 실행할 때가 효율적이 때도 있다.

 

3. 지연 로딩 활용

예사 상황

  • 회원은 팀 하나에만 소속 (N:1) / 회원, 팀이 자주 함께 사용 (즉시로딩)
  • 회원은 여러 주문내역을 가진다 (1:N) / 회원, 주문 내역은 가끔 함께 사용 (지연로딩)
  • 주문내역은 상품정보를 가진다 (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;
    
    //getter, setter...
}

3.1 프록시와 컬렉션 래퍼

  • 하이버네이트가 엔티티를 영속 상태로 만들 때
  • 엔티티에 컬렉션이 있다면 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는 것이 컬렉션 래퍼
  • (컬렉션을 추적하고 관리 할 목적)
  • 컬렉션 래퍼도 컬렉션에 대한 프록시 역할을 한다.

3.2 JPA 기본 fetch 전략

  • 연관된 엔티티가 하나라면 즉시 로딩
  • 컬렉션이면 지연 로딩(컬렉션을 로딩하는 것에는 비용이 많이 들고, 많은 데이터가 로딩될 수 있기에)
  • 항상 지연 로딩을 기본으로 하고 필요할 때만 즉시 로딩을 한다고 생각하면 된다.
  • 컬렉션을 즉시로딩 한다면 항상 OUTER JOIN이 사용된다. (ex. 회원이 한 명도 없는 팀을 내부조인하면 팀까지 조회가 안되는 문제가 발생하기에)

 

4. 영속성 전이: CASCADE

  • 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고 싶다면 영속성 전이 기능을 사용
  • 이때 CASCADE를 사용
  • +) JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
  • 연관관계 매핑과는 관련이 없다. (연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐)

4.1 저장

@Entity
public class Parent{
    @OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST) // 옵션 설정
    private List<Child> children = new ArrayList<Child>();
    ...
}
/////////////////
private static void example(EntityManager em){
    Child child1 = new Child();
    Child child2 = new Child();
    
    Parent parent = new Parent();
    child1.setParent(parent); // 연관관계 추가
    child2.setParent(parent);
    
    parent.getChildren().add(child1);
    parent.getChildren().add(child2);
    
    // 부모, 연관된 자식들 저장
    em.persist(parent)
}

4.2 삭제

기존이라면 각각의 엔티티를 하나씩 제거애햐 하지만 CascadeType.REMOVE로 설정이 되어있다면 연관된 엔티티도 함께 삭제가 된다

// 기존
Parent findParent = em.find(Parent.class, 1L);
Child findChild1 = em.find(Child.class, 1L);
Child findChild2 = em.find(Child.class, 2L);

em.remove(findChild1);
em.remove(findChild2);
em.remove(findParent);

// CascadeType.REMOVE사용 시
Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);

4.3 CASCADE의 종류

public enum CascadeType{
    ALL,
    PERSIST,
    MERGE,
    REMOVE,
    REFRESH,
    DETACH
}
// PERSIST, REMOVE는 바로 전이가 발생되지 않고 플러시를 호출할 때 전이가 발생

 

5. 고아 객체

  • JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제할 수 있게 한다. (고아객체(ORPHAN) 제거)
  • 자식엔티티의 참조만 제거하면 자동으로 삭제되도록 할 수 있다.
@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<Child>();
    ...
}
/////////////
Parent parent1 = em.find(Parent.class. id);
parent1.getChildren().remove(0); // 자식 엔티티를 컬렉션에서 제거
////////// 모든 자식 엔티티를 제거하는 방법
parent1.getChildren().clear();
  • 참조하는 곳이 하나일 때만 사용해야 한다.(특정 엔티티가 개인 소유하는 엔티티에만 이 기능을 적용해야 한다.)
  • @OneToOne, @OneToMany에서만 사용할 수 있다.
  • 부모를 제거하면 자식은 고아가 되고, 자식 또한 제거되기에 CascadeType.REMOVE를 설정한 것과 같이 동작한다.

 

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

  • CascadeType.ALL + orpahnRemoval = ture 동시 사용 시
  • 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있다.
  • 자식을 저장하려면 부모에 등록만 하면 되고, 자식을 삭제하려면 부모에서 제거만 하면 된다.

 

 

 

 

Comments