출처 : https://www.inflearn.com/course/ORM-JPA-Basic
자바 ORM 표준 JPA 프로그래밍 - 기본편 강의 - 인프런
회사땜에 매일 바쁜 와중에 학원이라도 다닐까 생각했는데 마침 JPA 강의가 생겨서 꿀 타이밍이네요. 저는 이 전에 JPA 책을 보고 공부 했었는데요 궁금했던 점, 업무에 적용하며 고민하고 해결하
www.inflearn.com
프록시
Member에 TEAM이 연관관계로 매핑되어 있을 때 username만 가져오고 싶은 경우 굳이 team까지 조회를 해야할까??
JPA에는 em.getReference() 참조를 가져오는 메서드가 있음.
em.find()는 데이터베이스를 통해서 실제 엔티티 객체를 조회하는 것.
em.getReference는 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회 (DB에 쿼리가 나가지 않는데 객체가 조회됨)
특징
- 실제 클래스를 상속 받아서 만들어짐
- 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨 (이론상)
- 프록시 객체는 실제 객체의 참조(target)를 보관
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
- 프록시 객체는 원본 엔티티를 상속 받음, 따라서 타입 체크 시 주의해야함(== 비교 안됨, 대신 instatnce of 사용)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환 (반대도 마찬가지로 프록시로 조회하면 다음에 조회해도 프록시를 반환함.)
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 (em.detach(), em.close() , em.clear() 후 프록시를 초기화하면 영속성 컨텍스트의 도움을 받을 수 없기 때문에 값을 가져올 수 없음, 하이버네이트에서는 org.gibernate.LayInitializationException 예외)
프록시 객체의 초기화
- 사용자가 getName() 요청
- 프록시 객체 내부에 target에서 name 있는지 확인
- 프록시에 값이 없을 때 초기화 요청
- 영속성 컨텍스트가 DB에서 조회 후 실제 Member 객체를 가져옴
- Member target에 실제 Memer 객체를 연결
username을 가져오는 시점에 target에 값이 없으면 db에서 가져오고 그 다음부터는 target에 값이 있으니까(이미 초기화 됨) 다시 db에서 조회하지 않음.
프록시 메서드
public class JpaMain {
public static void main(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUsername("user1");
em.persist(member);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member.getId());
//프록시 인스턴스의 초기화 여부 확인 false
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
tx.commit();
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
emf.close();
}
}
프록시 인스턴스의 초기화 여부 확인
emf.PersistenceUnitUtil.isLoaded(Object entity) : true or false
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember = " + refMember.getClass());
프록시 클래스 확인 방법
entity.getClass().getName() 출력(..javasist.. or HibernateProxy...)
refMember.getUsername(); // 강제 초기화 1
Hibernate.initialize(refMember); // 강제 초기화 2
프록시 강제 초기화 (Hibernate가 지원하는 것, JPA 표준은 강제 초기화 없음)
org.hibernate.Hibernate.initialize(entity);
강제 호출 : member.getName()
지연 로딩
Member 클래스
@Entity
public class Member extends BaseEntity{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Team team;
fetch 속성의 FetchType.LAZY로 설정하면 지연 로딩 = 프록시 객체(가짜)로 조회 = Member 클래스만 db에서 조회
JPA 클래스
// Team 저장
Team team = new Team();
team.setName("teamA");
em.persist(team);
// Member 저장
Member member = new Member();
member.setUsername("user1");
member.setTeam(team);
em.persist(member);
// 영속성 컨텍스트 제거
em.flush();
em.clear();
// Member만 찾을 때
Member m = em.find(Member.class, member.getId());
// 클래스 확인하면 프록시 객체로 나옴
System.out.println("m.team = " + m.getTeam().getClass());
// Team의 내부 타겟에 접근할 때 초기화 된다. m.getTeam()은 초기화 안 됨.
System.out.println("==============");
m.getTeam().getName();
System.out.println("==============");
지연로딩은 Team은 거의 사용하지 않고 Member만 조회하는 경우가 많을 때 사용
즉시 로딩
실무에서 사용XX (JPQL fetch 조인이나 엔티티 그래프 기능을 사용)
- 즉시로딩은 대부분 Team과 Member를 함께 조회할 때 사용 = Member 조회 시 항상 Team도 조회
- 즉시 로딩을 적용하면 예상치 못한 SQL이 발생
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
- @ManyToOne, @OneToOne은 기본이 즉시 로딩 => LAZY로 설정해야 함.
- @OneToMany, @ManyToMany는 기본이 지연 로딩
Member 클래스
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn
private Team team;
fetch 속성을 FetchType.EAGER로 설정
JPA 클래스
// Team 저장
Team team = new Team();
team.setName("teamA");
em.persist(team);
// Member 저장
Member member = new Member();
member.setUsername("user1");
member.setTeam(team);
em.persist(member);
// 영속성 컨텍스트 제거
em.flush();
em.clear();
// 즉시 로딩으로 설정하면 Member를 조회할 때 Team도 조인해서 가져옴
Member m = em.find(Member.class, member.getId());
// 프록시가 아닌 실제 엔티티
System.out.println("m.team = " + m.getTeam().getClass());
// 이미 조회된 Team 출력됨. 쿼리 실행 X
System.out.println("==============");
System.out.println(m.getTeam().getName());
System.out.println("==============");
tx.commit();
영속성 전이 (CASCADE)
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용
- 연관관계 매핑과 관련 없음
- Parent만 Child를 관리하거나 연관할 때 사용(Child가 다른 곳과 관계 있다면 사용X, 예) 첨부파일 - 첨부파일 경로)
- 예 : 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장.
옵션
- ALL : 모두 적용, 모든 라이프 사이클 맞춤
- PERSIST : 영속, 저장할 때만 사용
- REMOVE : 삭제
- MERGE : 병합
- REFRESH : REFRESH
- DETACH : DETACH
Parent 클래스
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> childList = new ArrayList<>();
// 연관관계 편의 메서드
public void addChild(Child child){
childList.add(child);
child.setParent(this);
}
...
}
Child 클래스
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
...
}
JPA 클래스
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(child1);
em.persist(child2);
em.persist(parent);
tx.commit();
원래는 child와 parent를 모두 persist 해야함.
Parent 클래스
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
cascade 옵션을 CascadeType.ALL로 변경
JPA 클래스
hild child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
tx.commit();
parent만 persist해도 child까지 모두 insert 됨.
고아 객체
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티 자동 삭제하는 기능
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 인식하고 삭제
- 참조하는 곳이 하나 일 때 사용해아함.
- @OneToOne, @OneToMany만 가능
- CascadeTypd.REMOVE처럼 동작 (
Parent 클래스
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
orphanRemoval = true로 변경
JPA 클래스
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
// Parent의 Child를 삭제하면 연관관계가 끊어진 Child도 DB에서 함께 삭제되는 delete 쿼리가 실행됨.
findParent.getChildList().remove(0);
tx.commit();
영속성 전이 + 고아 객체, 생명주기
- CascadeType.ALL + orphanRemoval=true
- 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거.
- 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음. (DAO와 리포지토리 없어도 됨)
- 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용.
'코딩 > JPA' 카테고리의 다른 글
[JPA] 객체지향 쿼리 언어(JPQL) - 기본 문법(1) (0) | 2024.02.05 |
---|---|
[JPA] 값 타입 (0) | 2023.12.07 |
[JPA] 상속관계 매핑 (0) | 2023.12.04 |
[JPA] 다양한 연관관계 매핑 (0) | 2023.12.04 |
[JPA] JPA 연관관계 매핑 (0) | 2023.12.02 |