elevne's Study Note
JPA - Proxy, Cascade, Orphan 본문
Proxy
엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다. (예를 들어, 회원 엔티티를 조회할 때 연관된 팀 엔티티는 비즈니스 로직에 따라 사용될 수도, 그러지 않을 수도 있다)
@Entity
@Data
public class Member {
@Id @GeneratedValue @Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
@Data
public class Team {
@Id @GeneratedValue @Column(name = "TEAM_ID")
private Long id;
private String name;
}
위에서 팀 정보는 제외한 회원 정보만 사용할 때에는 팀 엔티티까지 데이터베이스에서 조회하는 것은 비효율적이다. JPA 는 이러한 문제를 해결하기 위해 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데, 이를 지연로딩이라고 한다. (team.getName() 과 같이 팀 엔티티의 값을 실제 사용하는 시점에 디비에서 데이터를 조회하는 것) 지연로딩 기능을 사용하기 위해서는 실제 엔티티 객체 대신에 디비 조회를 지연할 수 있는 가짜 객체가 필요한데, 이를 프록시 객체라고 한다.
JPA 에서 식별자로 엔티티 하나를 조회할 때에는 EntityManager.find() 메소드를 사용한다. 이렇게 엔티티를 직접 조회하면 조회한 엔티티를 실제 사용하든 사용하지 않든 데이터베이스를 조회하게 된다. 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고자 한다면 EntityManager.getReference() 메소드를 사용하면 된다.
public void getReferenceTest() {
Member member = em.getReference(Member.class, 1L);
System.out.println(em.getReference(Member.class, 1L).getClass());
}
위와 같이 호출하면 JPA 는 데이터베이스를 호출하지 않고 데이터베이스 접근을 위임한 프록세 객체를 반환한다. 프록시 클래스는 실제 클래스를 상속받아서 만들어지므로 실제 클래스와 겉 모양이 같다. 프록시 객체는 실제 객체에 대한 참조를 보관, 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
이러한 프록시 객체는 member.getName() 처럼 실제 사용될 때 디비를 조회해서 실제 엔티티 객체를 생성하는데, 이를 프록시 객체의 초기화라고 한다. 프록시 객체는 주로 연관된 엔티티를 지연로딩할 때 사용된다. 위 멤버와 팀의 경우에 팀은 언제 조회하는 것이 좋을까? JPA 는 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있도록 두 가지 방법을 제공한다.
- 즉시로딩: 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다. (em.find(Member.class, 1L); 호출 시 팀도 함께 조회되는 것. 대부분의 JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인쿼리를 사용한다 / 주의해야 할 점으로, 만약 외래 키가 null 값을 허용한다면 LEFT OUTER JOIN 이 사용된다는 점이다. 외부 조인보다 내부 조인이 성능과 최적화에서 유리하다. 외래 키에 NOT NULL 제약조건이 있다면 내부조인만 사용해도 된다. @JoinColumn 에 nullable=false 로 설정한다 / @ManyToOne(fetch=FetchType.EAGER) )
- 지연로딩: 엔티티를 실제 사용할 때 조회된다. (위 예와 같은 경우 / @ManyToOne(fetch=FetchType.LAZY) )
Fetch 속성의 기본 설정값은 아래와 같다.
@ManyToOne, @OneToOne : FetchType.EAGER
@OneToMany, @ManyToMany : FetchType.LAZY
JPA 의 기본 fetch 전략은 연관된 엔티티가 하나면 즉시 로딩, 컬렉션이면 지연로딩을 사용한다. (컬렉션을 로드하는 것은 비용이 많이 들고, 잘못하면 너무 많은 데이터를 로드할 수 있기 때문) 추천되는 방법은 모든 연관관계에 지연로딩을 사용하는 방법이라고 한다. 그리고 애플리케이션 개발이 어느정도 완료단계에 왔을 때 실제 사용하는 상황을 보고 꼭 필요한 곳에서만 즉시 로딩을 사용할 수 있도록 최적화하면 된다고 한다.
또, FetchType.EAGER 을 사용할 때 주의해야할 점이 있다. 우선 컬렉션을 2 개 이상 즉시 로드하는 것은 권장되지 않는다. 컬렉션과 조인한다는 것은 데이터베이스 테이블이 일대다 조인이 된다는 것인데, 일대다 조인은 겨로가 데이터가 다 쪽에 있는 수만큼 증가하게 된다. 2 개 이상 조인하게 되면 (A 테이블이 N, M 테이블과 조인한다면) SQL 실행 결과가 N x M 이 되며 너무 많은 데이터를 반환할 수 있고, 결과적으로 애플리케이션 성능이 저하될 수 있다. 또, 컬렉션 로딩은 항상 OUTER JOIN 이 사용된다. (위 회원과 팀의 경우에서 회원이 팀을 조회하는 것은 INNER JOIN 이 가능하지만, 반대로 팀이 회원을 조회할 때에는 해당 팀에 아무도 소속되지 않았을 수도 있기 때문에 INNER JOIN 시 팀까지 데이터가 조회되지 않을 수 있다)
영속성 전이: CASCADE
특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고싶으면 영속성 전이 기능을 사용하면 된다. JPA 는 Cascade 옵션으로 영속성 전이를 제공한다.
@Entity
@Data
public class Parent {
@Id @GeneratedValue @Column(name = "PARENT_ID")
private Long id;
@OneToMany(mappedBy = "parent")
private List<Child> children = new ArrayList<Child>();
}
@Entity
@Data
public class Child {
@Id @GeneratedValue
private Long id;
@ManyToOne
private Parent parent;
}
JPA 에서 엔티티를 저장할 때 연관된 엔티티는 영속 상태여야 한다. 위 Parent, Child 를 저장하는 것을 생각해보면, 부모 엔티티를 영속 상태로 만든 다음 자식 엔티티도 각각 영속 상태로 만들어야 할 것이다. 이럴 때 영속성 전이를 사용하면 부모만 영속 상태로 만들어도 연관된 자식까지 한 번에 영속상태로 만들 수 있다. (CascadeType.PERSIST 를 사용한다)
@Entity
@Data
public class Parent {
@Id @GeneratedValue @Column(name = "PARENT_ID")
private Long id;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<Child>();
}
@Test
public void cascadeTest1() {
EntityTransaction tx = em.getTransaction();
tx.begin();
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);
tx.commit();
}
영속성 전이는 엔티티를 삭제할 때에도 사용할 수 있다. cascade=CascadeType.REMOVE 를 사용하면 된다. 부모 엔티티를 삭제하면 연관된 자식 엔티티도 함께 삭제된다.
이 외에도 ALL (모두 적용), MERGE (트랜잭션이 종료되고 detach 상태에서 연관 엔티티를 추가하거나 변경된 이후에 부모 엔티티가 merge 를 수행하게 되면 변경사항이 적용됨), REFRESH (상위엔티티 새로고침 시 연관된 엔티티도 새로고침한다), DETACH (부모 엔티티가 detach 를 수행하면 연관된 엔티티도 detach 상태가 됨) 옵션을 사용할 수 있다.
Orphan
JPA 는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데, 이를 고아객체 제거라고 한다. 이 기능을 사용하여 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하고 자식 엔티티를 자동 삭제되게 할 수 있다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<Child> children = new ArrayList<Child>();
@Test
public void orphanTest() {
EntityTransaction tx = em.getTransaction();
tx.begin();
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);
em.flush();
parent.getChildren().clear();
tx.commit();
}
위와 같이 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아객체로 보고 삭제하는 기능이 고아객체 제거이다. (이 기능은 참조하는 곳이 하나일 때만 사용해야 한다)
Reference:
자바 ORM 표준 JPA 프로그래밍
'Backend > JPA 프로그래밍' 카테고리의 다른 글
JPA - JPQL (0) | 2023.07.13 |
---|---|
JPA - Embedded / Collection Value Type (0) | 2023.07.12 |
JPA 고급매핑 (0) | 2023.07.09 |
JPA N:M 관계 (0) | 2023.07.06 |
JPA 연관관계 매핑 (0) | 2023.07.05 |