다음과 같이 연관된 객체, Member 객체와 Team 객체가 있다고 하자.
- Member만 조회하고 싶은 경우, 굳이 Team 엔티티까지 데이터베이스에서 같이 조회하는 것을 비효율적
- 어떤 엔티티를 조회할 때, 그 엔티티와 연관된 모든 앤티티를 조회함으로써 생기는 낭비 방지 위해 지연로딩 개념 사용
- 지연로딩: 연관된 객체를 처음부터 DB에서 조회하지 않고, 실제 사용하는 시점에서 DB에서 조회
- 회원 엔티티만 조회해서 사용 가능
- team.getName()과 같이 Team 엔티티의 값을 실제 사용하는 시점에 DB에서 team 엔티티에 필요한 데이터를 조회 가능
-> 지연로딩 개념 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체, '프록시 객체' 필요
- 식별자로 엔티티 하나를 조회할 때는 EntityManager.find() 사용
Member member = em.find(Member.class, "memeber1");
- em.find()는 영속성 컨텍스트에 엔티티가 없으면 DB 조회
- 이렇게 엔티티를 직접 조회하면 조회한 엔티티의 사용 여부에 상관없이 DB 조회
- 엔티티를 실제 사용하는 시점까지 DB 조회를 미루고 싶으면 EntityManager.getReference() 사용
- 실제 클래스를 상속 받아 만들어지기 때문에 똑같은 모양
-> 사용하는 입장에서는 이것이 진짜 객체인지, 프록시 객체인지 구분하지 않고 사용하면 됨
- 프록시 객체는 실제 객체에 대한 참조 target 보관
- 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출
- 처음에는 아직 DB 조회를 하지 않았기 때문에 target에 참조값이 없음
- 객체의 초기화: 프록시 객체는 member.getName()처럼 실제 사용될 때 DB를 조회해서 실제 엔티티 객체의 생성을 요청하는 것
Member member = em.getReference(Member.class, "id1");
member.getName();
- 동작 과정
- 처음에 member.getName() 호출
- 가져온 Member 프록시 객체의 target 값이 null(아직 DB 조회하기 전이므로)
- JPA가 영속성 컨텍스트에 초기화 요청
- 영속성 컨텍스트는 DB를 조회해서 실제 Entity 생성해서 반환
- 프록시 객체의 target에 반환된 실제 Entity 참조값 저장
- target.getName()이 호출되면서 결과 반환
- 프록시 객체는 처음 사용될 때 한번만 초기화
- 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아님
-> 초기화되면 프록시 객체를 통해 실제 엔티티에 접근할 수 있다는 것을 의미 - 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 함
-> 즉, == 비교 대신 instance of를 사용해야 함. - 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없기 때문에 em.getReference() 호출해도 프록시가 아닌 실제 엔티티 반환
- 초기화는 영속성 컨텍스트의 도움을 받아야 가능
-> 준영속상태일 때, 프록시 초기화하면 문제 발생(LazyInitializationException) -> 엔티티가 준영속성 상태가 되는 대표적인 경우는 트랜잭션이 종료돼서 영속성 컨텍스트가 close 된 경우 -> 실무에서 트랜잭션이 끝나고 영속성 컨텍스트를 조회하는 경우에 많이 마주치는 오류
- 프록시 객체는 파라미터로 전달받은 식별자 값을 보관
Team team = em.getReference(Team.class, "team1"); //식별자 보관
team.getId(); // 초기화 되지 않습니다.
- 엔티티 접근 방식이 프로퍼티(@Access(AccessType.PROPERTY))일 경우
프록시 객체는 식별자 값을 가지고 있기 때문에, 식별자 값을 조회하는 team.getId() 호출해도 프록시 초기화 x - 엔티티 접근 방식이 필드(@Access(AccessType.FIELD))일 경우
JPA는 getId() 메소드가 id만 조회하는 메소드인지 아니면 다른 필드까지 활용해서 다른 일을 하는 메소드인지 알 수 없기 때문에 프록시 객체를 초기화
- 프록시는 연관관계 설정할 때 유용
-> 연관관계 설정 시, 식별자 값만 사용하기 때문에 프록시를 사용하면 데이터베이스 접근 횟수 줄일 수 있음
-> 연관관계 설정할 때는 엔티티 접근 방식을 필드로 설정해도 프록시 초기화되지 않음
-> 멤버 엔티티와 팀 엔티티 연관관계 설정 시, 팀 엔티티를 DB에서 영속성 컨텍스트로 가져오지 않고 팀 엔티티의 식별자 값만 가지고 있는 프록시를 사용하면 DB 접근 횟수 줄일 수 있음
Member member = em.getReference(Member.class, "member1");
Team team = em.getReference(Team.class, "team1"); //식별자 보관
member.setTeam(team);
-
프록시 인스턴스의 초기화 여부 확인
-> PersistenceUnitUtil.isLoaded(Object entity) -> true/false 반환- 예제
boolean isLoad = em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(entity); System.out.println("isLoad = " + isLoad)
-
프록시 클래스 확인 방법(조회한 엔티티가 진짜 엔티티인지, 프록시 엔티티인지 확인)
-> entity.getClass().getName()으로 클래스명 직접 출력 -> 클래스명 뒤에 ..javassist.. 붙으면 프록시 엔티티- 예제
System.out.println("memberProxy =" + member.getClass().getName()); // 결과 : memberProxy = jpabook.domain.Member_$$_javassist_0
-
프록시 강제 초기화
- 하이버네이트의 initialize() 메소드를 사용하면 프록시 강제 초기화 가능
- JPA 표준은 강제 초기화 없음
- 즉시 로딩 : 엔티티 조회할 때 연관된 엔티티도 함께 조회
- 지연 로딩 : 연관된 엔티티 실제 사용할 때 조회
- Member를 조회할 때 연관된 Team도 함께 조회해야 할까? -> 비즈니스 로직에 따라 달라짐
- Member와 Team 을 자주 함께 사용한다면? -> 한번에 같이 조회하는 것이 좋음
- @ManyToOne의 fetch 속성을 FetchType.EAGER로 지정
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
- 즉시 로딩을 최적화하기 위해 조인 쿼리 사용 (쿼리 2번 실행)
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
ON M.TEAM_ID=T.TEAM_ID
WHERE
M.MEMBER_ID='member1'
-> DB에서 Member 조회할 때 Team도 함께 조회
-> 2번 쿼리를 날리지 않고 join해서 한번에 가져옴
-> 한번에 엔티티 조회해서 가져오기 때문에 Team엔티티도 프록시가 아니라 실제 엔티티를 가져옴
- member 정보만 사용
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
//지연로딩 전략, team 객체는 프록시 객체로 조회한다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team
...
}
-> member1 조회할 때 연관된 team 엔티티는 DB에서 조회하지 않고 프록시로 초기화
-> 실제로 team.getName()과 같이 team 엔티티를 사용하는 시점에 프록시가 실제 엔티티 가리키도록 초기화
- JPA의 기본 패치 전략
- @ManyToOne, @OneToOne : 즉시 로딩(default)
- @OneToMany, @ManyToMany : 지연 로딩(default)
- JPA의 기본 패치 전략은 연관된 엔티티가 하나면 즉시로딩을, 컬렉션이면 지연 로딩 사용
- 실무에서는 가급적 지연 로딩만 사용 추천
- 즉시 로딩 적용하면 예상치 못한 SQL 발생 가능
-> 테이블에 연관관계 복잡하게 되어 있을 때 즉시 로딩 사용하면 join 여러번 발생 -> 성능 감소!!- 결론 : 무조건 다 지연 로딩 써라. 그 후, 꼭 필요한 곳에만 즉시 로딩을 최적화
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용
- 쉽게 말해, 영속성 전이 사용하면 부모 엔티티 저장할 때 자식 엔티티도 함께 저장 가능
- JPA에서 엔티티 저장할 때 연관된 모든 엔티티는 영속 상태여야 함
이때, 영속성 전이 안해주면 자식 개수만큼 em.persist(child) 다 해줘야 함 -> 귀찮아!!
- 영속성 전이 사용 X
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy="parent")
private List<Child> children = new ArrayList<Child> ();
...
private static void saveNoCascade (EntityManager em) {
//부모 저장
Parent parent = new Parent();
em.perists(parent);
//1번 자식 저장
Child child1 = new Child();
child1.setParent(parent); // 자식과 부모 연관관계 설정
parent.getChildren().add(child1); // 부모 -> 자식
em.persist(child1);
//2번 자식 저장
Child child2 = new Child();
child2.setParent(parent); // 자식과 부모 연관관계 설정
parent.getChildren().add(child2); // 부모 -> 자식
em.persist(child2);
- 영속성 전이 사용 O
@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); // 연관관계 추가
child2.setParent(parent); // 연관관계 추가
parent.getChildren().add(child1);
parent.getChildren().add(child2);
//부모 저장, 연관된 자식들 저장
em.persist(parent);
- 주의
- 영속성 전이는 연관관계 매핑하는 것과 아무 관련 없음
- 그저 엔티티 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함만 제공
- 그래서, 위의 코드에서 양방향 연관관계 추가한 다음 영속 상태로 만들어 줘야 함
- 영속성 전이 사용 X
Parent findParent = em.find(Parent.class, 1L);
Parent findChild1 = em.find(Child.class, 1L);
Parent findChild2 = em.find(Child.class, 2L);
em.remove(findChild1);
em.remove(findChild2);
em.remove(findParent);
- 영속성 전이 사용 O
Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);
- 코드 실행하면 DELETE SQL 3번 실행되고 부모와 연관된 자식 모두 삭제
- 이때, 삭제 순서는 외래 키 제약조건 고려해 자식 먼저 삭제하고 부모 삭제
- ALL : 모두 적용
- PERSIST : 영속
- REMOVE : 삭제
- MERGE : 병합
- REPRESH : REPRESH
- DETACH : DETACH
→ 여러 속성 같이 사용 가능 (cascade ={CascadeType.PERSIST, Cascade.REMOVE}) - 참고 : PERSIST, REMOVE는 em.persist(), em.remove()를 실행할 때 바로 전이가 발생하지 않고, 플러시 호출할 때 전이 발생
- 고아 객체 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티
- 고아 객체 제거 : 고아 객체를 자동으로 삭제하는 기능 제공
@Entity
public class Parent {
@Id @GeneratedValue
private Loing id;
@OneToMany (mappedBy = "parent", orphanRemoval= true)
private List<Child> children = new ArrayList<Child> ();
...
}
- orphanRemoval = true
Parent parent1 = em.find(Parent.class,id);
parent1.getChildren().remover(0); // 자식 엔티티를 컬렉션에서 제거 -> 부모 객체와의 연관관계 끊어짐
- 실행 결과 DELETE 쿼리 발생
DELETE FROM CHILD WHERE ID = ?
- 주의
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아로 보고 삭제
- 참조하는 곳이 하나일 때 사용해야 한다!!
- 즉, 특정 엔티티가 개인 소유하는 엔티티일때만 이 기능 적용해야 함
-> @OneToMany, @OneToOne에만 orphanRemoval 사용 가능
- 즉, 특정 엔티티가 개인 소유하는 엔티티일때만 이 기능 적용해야 함
- CascadeType.ALL + orphanRemoval = true 를 동시에 사용하면?
- 일반적으로 엔티티는 EntityManager.persist()를 통헤 영속화되고 EntityManager.remove() 통해 제거 -> 즉, 엔티티가 스스로 생명주기 관리
- 두 옵션 모두 활성화하면 부모 엔티티 통해 자식 엔티티의 생명주기 관리 가능
- 예제
- for 자식 저장 -> 부모에 등록
Parent parent = em.find(Parent.class,parentId); parent.addChild(child1);
- for 자식 삭제 -> 부모에서 삭제
Parent parent = em.find(Parent.class,parentId); parent.getChildren().remove(removeObject);
- for 자식 저장 -> 부모에 등록