연관관계를 맺은 Member 와 Team 객체에서
Member 만 조회할 때 Member 객체 안에는 Team 객체가 들어가 있을까?
만약 그렇다면 Member만 사용하고 싶을 때는 어떻게 해야하나 ?
JPA는 프록시 기술을 통해서 이 문제를 해결하였다.
✔️ 프록시
실제 클래스를 상속받아서 만들어진 가짜
위임
프록시 객체는 실제 객체의 참조를 보관 (Entity target 필드 가짐)
프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
1. 멤버 프록시 조회
Member member = em.getRefernce(Member.class, id);
2. member.getName() 호출
member.getName();
이 순간부터 프록시는 실제 member 객체 요청을 영속성 컨텍스트에게 날린다.
영속성 컨텍스트는 해당 member를 DB에서 조회하고 실제 Entity를 생성한 뒤
프록시가 실제 member 객체를 참조하고 (target) ,
target.getName() 을 호출한다. (위임)
프록시의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시 객체를 초기화할 때 프록시 객체가 실제 엔티티로 전환 X
- 초기화되면 프록시 객체를 통해서 실제 엔티티 접근 가능
- 프록시 객체는 원본 엔티티를 상속받음
- 따라서 타입 체크시 주의 (== 비교 x, instance of 사용)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있다면 em.getReference() 호출해도 실제 엔티티 반환
- 준영속 상태일때 프록시 초기화시 문제 발생 ( org.hibernate.LazyInitializationException )
Member member = new Member();
member3.setUsername("회원3");
em.persist(member);
em.flush();
em.clear();
Member reference = em.getReference(Member.class, member.getId());
em.clear(); // 영속성 컨텍스트 초기화
reference.getUsername(); // 준영속 상태에서 초기화 요청
참고: 프록시 객체도 영속성 컨텍스트에서 관리한다.
- 프록시 확인
- PersistenceUnitUtil.isLoaded(Object entity)
EntityManagerFactory emf;
emf.getPersistenceUnitUtil().isLoaded(reference); // false: 프록시 상태
- 프록시 클래스 확인 방법
- entity.getClass().getName() 출력 (HibernateProxy..)
jpql.jpql.Member$HibernateProxy$9IaDAX2A // 프록시라는 의미
- 프록시 강제 초기화
- org.hibernate.Hibernate.initialize(entity);
참고 : JPA 표준은 강제 초기화가 없기에 강제호출해야한다. getName() 등..
❓ 프록시 getId() 초기화
프록시는 식별자 값을 가지고 있다.
따라서 getId()를 해도 초기화되지 않는다.
연관관계를 설정할때 프록시를 사용해서 데이터베이스 접근 횟수를 줄일 수 있다.
✔️ 즉시 로딩과 지연 로딩
💠 즉시 로딩
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
@JoinColumn(name = "TEAM_ID")
private Team team;
즉시 로딩시 member 엔티티를 조회하면 JPA 구현체는 가능하면 조인을 사용해서 해당 member 엔티티와 연관된 Team까지 SQL 한 번에 조회해서 가져온다.
💠 지연 로딩
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
@JoinColumn(name = "TEAM_ID")
private Team team;
지연로딩시 member를 조회하면 연관관계인 team객체는 프록시 객체로 가져온다.
후에 member.getTeam().getName()등을 호출하면 그때 쿼리를 날려서 실제 엔티티를 가져온다.
참고 : getObject()는 초기화 되지 않는다.
실질적인 사용이 있어야한다. ex) getName();
프록시와 즉시로딩 주의
- 가급적 (반드시..) 지연 로딩만 사용
- 즉시 로딩은 JPQL에서 N+1 문제 발생
- member와 team이 N:1로 10개씩 연관되어있다고 하자
- 1개의 쿼리를 날려서 member를 조회했는데 즉시로딩 이기 때문에 각 member에 연관된 team을 조회하는 쿼리가 10개 생성되어 날린다.
- 1 + 10 즉 N+1 문제발생
- @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 LAZY로 설정
🌱 지연 로딩 활용
모든 연관관계에서 지연 로딩 사용
즉시 로딩같은 기능이 필요하다면 JPQL의 fetch 조인, 엔티티 그래프 기능을 사용
✔️ 영속성 전이 CASCADE
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때
ex) 부모저장시 자식도 함께 저장
@OneToMany(mappedBy="parent", cascade = CascadeType.PERSIST)
테스트
Parent parent = new Parent();
Child child = new Child();
parent.getChildList().add(child);
em.persist(parent);
em.flush();
em.clear();
영속성 전이는 연관관계를 매핑하는 것과 아무 관련 없다
엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공
CASCADE 종류
- ALL : 모두 적용
- PERSIST : 영속
- REMOVE : 삭제
- MERGE : 병합
- REFRESH : REFRESH
- DETACH : DETACH
✔️ 고아 객체
고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동 제거
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
orphanRemoval = true // 고아 객체 제거
Parent parent = new Parent();
Child child = new Child();
parent.getChildList().add(child);
child.setParent(parent);
em.persist(parent);
em.flush();
em.clear();
Parent parent1 = em.find(Parent.class, parent.getId());
parent1.getChildList().remove(0);
주의 사항
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 참조하는 곳이 하나일 때 사용
- 특정 엔티티가 개인 소유할 때 사용
- @OneToOne, @OneToMany만 가능
참고 : 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작
✔️ 영속성 전이 + 고아 객체, 생명주기
cascade = CascadeType.ALL, orphanRemoval = true
스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
부모 엔티티가 자식의 생명주기를 관리하는 형태
도메인 주도 설계의 Aggregate Root 개념을 구현할 때 유용
ps. 도메인 주도 설계 책을 읽어봐야겠다.
🔖 학습내용 출처
자바 ORM 표준 JPA 프로그래밍 - 기본편 / 김영한
김영한. (2021). 자바 ORM 표준 JPA 프로그래밍. 에이콘출판사
'Back-End > JPA' 카테고리의 다른 글
JPA - JPQL (0) | 2024.07.12 |
---|---|
JPA - 값타입 (0) | 2024.07.12 |
JPA - 고급 매핑 (0) | 2024.07.09 |
JPA - 1:N 단방향과 양방향에 대해서 (0) | 2024.07.09 |
JPA - 다양한 연관관계 매핑 (0) | 2024.07.09 |