프록시
Member를 조회할 때 Team도 함께 조회를 해야 할까?
회원 이름과 소속 팀을 함께 출력해야 하는 경우에는 팀 객체도 조회해야 하지만, 회원 이름만 출력해야 하는 경우에는 팀 객체를 조회할 필요가 없다.
프록시 기초
- em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
프록시 엔티티 객체는 실제 클래스를 상속받아 만들기 때문에 겉모양이 동일하다. 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
프록시 객체는 실제 객체의 참조(target)을 보관한다. 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다. 이 때, target은 DB에 SELECT SQL을 날려 실제 객체 조회 시 가져온다.
프록시 객체의 초기화
Member member = em.getReference(Member.class, "id1");
member.getName();
getReference() 메소드를 사용했기 때문에 프록시 객체가 member에 반환된다. 그 후, getName()을 호출하면 프록시 객체는 영속성 컨텍스트에 초기화 요청을 한다. 영속성 컨택스트에서는 DB 조회를 해 해당 값에 해당하는 실제 엔티티를 생성한다. 그 후, 회원 프록시 객체에 참조값을 담아서 프록시 객체와 실제 객체를 연결한다
프록시 특징
1. 프록시 객체는 처음 사용할 때 한 번만 초기화하면 된다.
- 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능해진다.
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass()); // 프록시 객체 출력됨
System.out.println("findMember.username = " + findMember.getUsername()); // 프록시 객체 초기화
System.out.println("findMember = " + findMember.getClass()); // 여전히 프록시 객체 출력됨
2. 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크 시 주의해야 한다. (== 비교 실패, 대신 instanceOf 사용)
System.out.println(findMember.getClass() == Member.class); // 실패
System.out.println(findMember instanceof Member); // 성공
3. 영속성 컨텍스트에 찾는 엔티티가 이미 존재하면, em.getReference()를 호출해도 실제 엔티티가 반환된다.
- 영속성 컨텍스트에 이미 있다면, 굳이 프록시 객체를 사용할 필요가 없다,
Member m1 = em.find(Member.class, member1.getId()); // 영속성 컨텍스트에 저장
System.out.println("m1: " + m1.getClass());
Member ref = em.getReference(Member.class, member1.getId()); // 실제 엔티티 반환
System.out.println("ref: " + ref.getClass());
System.out.println("m1 == ref: " (m1 == ref)); // 성공
4. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 경우, 프록시를 초기화하면 문제 발생
- 프록시 객체를 초기화할 때, 영속성 컨텍스트가 직접 DB에 조회를 해야 하기 때문에 영속성 컨텍스트가 관리할 수 없는 준영속 상태이면 초기화가 불가능하다.
프록시 확인
- 프록시 인스턴스의 초기화 여부 확인 : PersistenceUtil.isLoaded(Object entity)
- 프록시 클래스 확인 방법 : entity.getClass().getName() 출력
- 프록시 강제 초기화 : org.Hibernate.initialize(entity);
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("isLoaded=" + emf.getPersistenceUtilUnit().isLoaded(refMember));
Hibernate.initialize(refMember);
System.out.println("isLoaded=" + emf.getPersistenceUtilUnit().isLoaded(refMember));
[참고] JPA 표준은 강제 초기화 없음
강제 호출 : member.getName()
즉시 로딩과 지연 로딩
즉시 로딩
엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
즉시 로딩을 사용하려면 @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개가 나와야 할 것 같지만, JPA 구현체는 가능하면 조인을 사용해서 SQL을 한 번에 함께 조회한다.
지연 로딩
연관된 엔티티를 실제 사용할 때 조회한다. 즉, 연관된 엔티티를 프록시로 조회한 후, 실제 사용해야 할 때 초기화하면서 데이터베이스를 조회한다.
지연 로딩을 사용하려면 @ManyToOne의 fetch 속성을 FetchType.LAZY로 설정하면 된다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.LAZY) //**
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 프록시 객체 조회
team.getName(); // 팀을 사용하는 시점에 초기화
프록시와 즉시 로딩 주의
- 가급적 지연 로딩만 사용해야 한다
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다
- 즉시 로딩은 JPQL에서 N+1문제를 일으킨다
- @ManyToOne, @OneToOne은 기본이 즉시 로딩이다. -> LAZY로 설정
- @OneToMany, @ManyToMany는 기본이 지연 로딩이다.
지연 로딩 활용 - 실무
- 모든 연관관계에 지연 로딩을 사용해라
- 실무에서 즉시 로딩을 사용하지 마라
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라
- 즉시 로딩은 상상하지 못한 쿼리가 나간다
영속성 전이와 고아 객체
영속성 전이 : CASCADE
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 한께 영속 상태로 만들고 싶을 때 영속성 전이 기능을 사용한다. JPA는 CASCADE 옵션으로 영속성 전이를 제공한다.
예를 들어, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 때 주로 사용한다. 만약 부모 1명에 자식 2명을 저장하려고 한다면, em.persist() 메서드를 3번 작성해야 한다. JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 하기 때문이다. 이럴 때 영속성 전이를 사용하면 부모만 영속 상태로 만들면 연관된 자식까지 한번에 영속 상태로 만들 수 있다.
@OneToMany(mappedBy = "parent", cascade = CascadeeType.PERSIST)
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.
CASCADE 종류
- ALL : 모두 적용
- PERSIST : 영속
- REMOVE : 삭제
- MERGE : 병합
- REFRESH : REFRESH
- DETACH : DETACH
고아 객체
JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공한다. 이 기능을 사용해서 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다. 즉, DELETE SQL이 나간다
@OneToMany(mappedBy = "parent", orphanRemoval = true)
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아객체로 보고 삭제하는 기능
- 참조하는 곳이 하나일 때만 사용해야 함
- 특정 엔티티가 개인 소유할 때 사용 가능
- @OneToOne, @OneToMany만 가능하다
영속성 전이 + 고아 객체, 생명주기
Cascade.Type.ALL과 orphanRemoval=true를 함께 사용하면 어떻게 될까?
두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
자식을 저장하려면 부모에 등록만 하면 된다 (cascade)
Parent parent = em.find(Parent.class, parentId);
parent.addChild(child1);
자식을 삭제하려면 부모에서 제거만 하면 된다 (orphanRemoval)
Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(removeObject);
'백엔드 > SPRING' 카테고리의 다른 글
[JPA 프로그래밍] 9. 객체 지향 쿼리 언어1 - 기본 문법 (0) | 2025.02.15 |
---|---|
[JPA 프로그래밍] 8. 값 타입 (0) | 2025.02.07 |
[JPA 프로그래밍] 6. 고급 매핑 (0) | 2025.02.05 |
[JPA 프로그래밍] 5. 다양한 연관관계 매핑 (0) | 2025.02.04 |
[JPA 프로그래밍] 4. 연관관계 매핑 기초 (0) | 2025.02.03 |