프록시
테이블을 조회해서 객체를 가져올 때 연관관계가 맺어진 객체까지 모두 조회하게 된다.
객체를 조회할 때 연관관계가 맺어진 객체는 안 가져오고 싶다면 어떻게 해야 할까?
- em.find() : 데이터베이스를 통해서 실제 엔티티 객체를 조회
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회 -> DB에 SQL은 전송되지 않지만 조회된다.
em.find()를 사용하면 호출 시점에 즉시 데이터베이스에 SQL이 전송되지만 em.getReference()을 사용하면 호출 즉시가 아닌 em.getReference()를 통해 가져온 객체를 사용하는 시점에 SQL이 전송된다.
User findUser = em.find(User.class, user.getId()); // 호출 시점에 즉시 데이터베이스에 SQL 전송
System.out.println("findUser.getName : "+findUser.getUsername());
System.out.println("findUser.getClass : "+findUser.getClass());
em.clear();
User getReferenceUser = em.getReference(User.class, user.getId());
System.out.println("getReferenceUser.getUsername : "+getReferenceUser.getUsername()); // 객체 사용 시점에 SQL 전송
System.out.println("getReferenceUser.getClass : "+getReferenceUser.getClass());
findUser.getClass : class infleranJpaBasic.example.User
getReferenceUser.getClass : class infleranJpaBasic.example.User$HibernateProxy$2eF9Aefj
em.getReference()로 가져온 객체는 Hibernate 내부 라이브러리를 사용하여 HibernateProxy 가짜 엔티티 객체로 가져온다.
- 실제 클래스를 상속받아서 만들어지며 실제 클래스와 겉모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.(이론상)
- 프록시 객체는 실제 객체의 참조(target)를 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 참조(target)에 있는 엔티티를 호출한다.
처음에는 실제 DB에서 조회한 적이 없기 때문에 프록시 객체의 참조(target)가 없다. 그러면 어떻게 동작하여 데이터를 가져올까?
1. em.getReference()를 통해 프록시 객체를 가지고 온다.
2. 실제 객체를 사용하는 시점( member.getName(); )에 JPA는 영속성 컨텍스트에 초기화를 요청한다.
3. 영속성 컨텍스트는 DB를 조회한다.
4. 영속성 컨텍스트가 실제 엔티티 객체를 생성한다.
5. 프록시 객체의 참조(target)과 실제 엔티티 객체를 연결한다.
프록시 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체는 유지가되고 내부 target에 값이 채워져 프록시 객체를 통해 실제 엔티티에 접근 가능하게 되는 것이다.
- 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크 시 주의해야 함( ==비교 실패, 대신 instance of 사용)
타입체크 시에는 instanceof를 사용하자.
System.out.println(findUser == getReferenceUser); // false
System.out.println(findUser instanceof User); // true
System.out.println(getReferenceUser instanceof User); // true
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
User u1 = em.find(User.class, user1.getId());
System.out.println("u1.getClass : "+u1.getClass()); // User
User u2 = em.getReference(User.class, user1.getId());
System.out.println("u2.getClass : "+u2.getClass()); // User
System.out.println(u1 == u2) // true
이미 User가 영속성 컨텍스트에 올라와 있기 때문에 프록시 객체를 반환할 필요가 없다.
- 반대로 영속성 컨텍스트에 찾는 엔티티가 프록시 객체로 있으면 em.find()를 호출해도 프록시 객체 반환.
User u1 = em.getReference(User.class, user1.getId());
System.out.println("u1.getClass : "+u1.getClass()); // froxy
User u2 = em.find(User.class, user1.getId());
System.out.println("u2.getClass : "+u2.getClass()); // froxy
System.out.println(u1 == u2); // true
다음과 같은 경우 em.find()를 해도 프록시 객체가 반환될 수 있다.
(우리는 프록시 객체이든 실제 객체이든 상관없이 동작하도록 개발해야 한다.)
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
User refUser = em.getReference(User.class, user1.getId());
System.out.println("refUser.getClass : "+refUser.getClass()); // froxy
em.detach(refUser); // 준영속상태로 변환
refUser.getUsername();
프록스 유틸리티 메서드
- 프록시 인스턴스의 초기화 여부 확인
: PersistenceUnitUtil.isLoaded(Object entity) → entityManagerFactory.getPersistenceUnitUtil().isLoaded(object) - 프록시 클래스 확인방법
entity.getClass().getname() 출력(..javasist.. or HibernateProxy...) - 프록시 강제 초기화
: org.hibernate.Hibernate.initialize(entity); - 참고: JPA 표준은 강제 초기화 없음
강제 호출: method.getNameI();
즉시로딩과 지연로딩
지연로딩
Member를 조회할 때 Team을 함께 조회하지 않아도 될 때
: 단순히 member의 정보만 필요한 비즈니스 로직
@Entity
@Getter @Setter
public class MemberR {
//...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
//...
}
@Entity
@Getter @Setter
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<MemberR> members = new ArrayList<>();
}
- @ManyToOne(fetch = FetchType.LAZY) : 지연로딩으로 설정
MemberR findMember = em.find(MemberR.class, member.getId());
System.out.println(findMember.getClass()); // MemberR
System.out.println(findMember.getTeam().getClass()); // Froxy
연관관계를 지연로딩을 설정할 경우 데이터 조회 시 JOIN 하지 않고 데이터를 가져오며 JOIN을 통해 가져와야 하는 연관관계 객체는 프록시 객체로 가져온다.
실제 연관관계 객체인 team의 객체를 사용하는 시점에 프록시 객체가 초기화되며 TEAM 데이터 조회 SQL이 전송된다.
즉시로딩
Member와 Team을 함께 자주 함께 사용할 때
@Entity
@Getter @Setter
public class MemberR {
//...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
//...
}
@Entity
@Getter @Setter
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<MemberR> members = new ArrayList<>();
}
- @ManyToOne(fetch = FetchType.EAGER) : 즉시로딩으로 설정
MemberR findMember = em.find(MemberR.class, member.getId());
System.out.println(findMember.getClass()); // MemberR
System.out.println(findMember.getTeam().getClass()); // Team
연관관계를 즉시로딩을 설정할 경우 데이터 조회 시 즉시 SQL이 전송되며 JOIN 하여 데이터를 가져오기 때문에 프록시 객체가 필요하지 않다.
프록시와 즉시로딩 주의
- 가급적 지연 로딩만 사용(특히 실무에서)
- 즉시로딩을 적용하면 예상하지 못한 SQL이 발생한다.
ex. 엔티티에 연관관계가 맺어진 엔티티가 많다면 em.find() 시 모든 테이블을 JOIN 한다. - 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
@Entity
@Getter @Setter
public class MemberR {
//...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
//...
}
List<MemberR> resultList = em.createQuery("select m from MemberR m", MemberR.class)
.getResultList();
// SQL : select * from MemberR
// SQL : select * from Team where TEAM_ID = ?
위 JPQL을 그대로 SQL로 번역하게 되면 MemberR을 가져오기 위한 SQL 수행 이후 바로 MemberR과 연관관계가 연결된 Team을 가져오기 위한 SQL을 다시 수행하게 된다 → N+1(1개의 쿼리를 날리면 +N개의 쿼리가 추가수행된다)
- @ManyToOne, @OneToOne은 디폴트가 즉시로딩이다. → LAZY로 직접 설정해야 함.
- @OneToMany, @ManyToMany는 디폴트가 지연로딩이다.
N+1 문제 해결책
1. 모든 연관관계를 전부 지연로딩으로 설정
그다음 가져와야 하는 엔티티에 한해서 fetch join을 사용해서 가져온다
List<MemberR> resultList = em.createQuery("select m from MemberR m join fetch m.team", MemberR.class)
.getResultList();
이렇게 실행하면 Team도 한 번에 조회하여 가져오기 때문에 더 이상 SQL이 전송되지 않는다( == N+1 발생하지 않는다).
지연 로딩 활용
- Member와 Team은 자주 함께 사용 → 즉시 로딩
- Member와 Order는 가끔 사용 → 지연 로딩
- Order와 Product는 자주 함께 사용 → 즉시 로딩
지연 로딩 활용 - 실무
- 모든 연관관계에 지연로딩을 사용해라!
- 실무에서 즉시 로딩을 사용하지 마라!
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라!(뒤에서 설명)
- 즉시로딩은 상상하지 못한 쿼리가 나간다.
영속성 전이(CASCADE)와 고아 객체
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용
ex. 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장
@Entity
@Getter @Setter
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);
}
}
@Entity
@Getter @Setter
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name="parent_id")
private Parent parent;
}
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.persist(child1);
em.persist(child2); // persist를 3번 호출해야한다.
em.persist()를 3번 호출해야 동작한다.
em.persist() 한 번으로 child 엔티티 객체까지 영속성 컨텍스트에 올릴 수 있는 방법이 없을까?
cascade 영속성 전이 적용
@Entity
@Getter @Setter
public class Parent {
//...
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
//...
}
@Entity
@Getter @Setter
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name="parent_id")
private Parent parent;
}
- @OneToMany에 cascade = CascadeType.ALL 적용
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); // persist를 1번으로 child 객체 까지 저장
parent만 em.persist() 해도 parent와 연관관계 가 맺어진 child도 같이 persist가 된다.
영속성 전이 : CASCADE - 주의!
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.
CASCADE의 종류
- ALL : 모두 적용, 모든 곳에서 라이프사이클을 맞출 때)
- PERSIST : 영속(저장할 때만 사용하고 싶을 때)
- REMOVE : 삭제
- MERGE : 병합
- REFRESH : REFRESH
- DETACH : DETACH
CASCADE 언제 사용해야 할까?
: 소유자가 하나일 때 (전이될 대상이 한 곳에서만 사용될 때) 사용하는 것이 좋다.
child에서 다른 엔티티로 나가는 것은 문제가 되지 않지만, 다른 엔티티가 child와 연관관계가 있다면 사용하면 안 된다.
즉, 해당 엔티티(Child)가 특정 엔티티(Parent)에 종속되지 않고 여러 군데서 사용된다면 사용하면 안 된다.
- 라이프 사이클이 동일할 때
- 단일 소유자 관계 일 때
고아 객체
고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
@Entity
@Getter @Setter
public class Parent {
//...
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
//...
}
- @OneToMany에 orphanRemoval = true 추가
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); // persist를 1번으로 child 객체 까지 저장
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);
부모 엔티티에서 자식 엔티티를 제거하면 DELETE SQL이 전송되는 것을 확인할 수 있다.
고아객체 - 주의
- 참조가 제거된 엔티티를 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.
- 참조하는 곳이 하나일 때 사용해야 한다.
- 특정 엔티티가 개인 소유할 때 사용해야 한다.
- @OneToOne, @OneToMany만 가능
- 참고 :
개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 orphanRemoval = ture 설정을 하면 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.
Parent findParent = em.find(Parent.class, parent.getId());
em.remove(findParent);
부모 엔티티 제거 시 자식 엔티티가 전부 삭제된 것을 확인할 수 있다.
Cascade.ALL + orphanRemovel = true
- 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화하거나 em.remove()로 제거할 수 있다.
라이프사이클을 영속성 컨텍스트를 통해서 관리한다. - 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 라이프사이클을 관리할 수 있다.
- 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용
해당 글은 인프런의 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 정리한 내용입니다.
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com
'JPA > JPA' 카테고리의 다른 글
[JPA] 값 타입 (0) | 2023.04.22 |
---|---|
[JPA] 고급매핑 - 상속관계 매핑, @MappedSuperClass 매핑 정보 상속 (0) | 2023.04.18 |
[JPA] 다양한 연관관계 매핑 - 다대다 (0) | 2023.04.14 |
[JPA] 다양한 연관관계 매핑 - 일대일 (0) | 2023.04.14 |
[JPA] 다양한 연관관계 매핑 - 다대일, 일대다 (0) | 2023.04.14 |
댓글