프록시
프록시 패턴에서 나오는 그 프록시이다
프록시란 원래의 객체 인스턴스를 상속받는 새로운 클래스를 칭한다
왜 프록시가 필요한지 생각해보자
Persist Context에서 객체를 EntityManager.find()로 가져올 때, 만약 객체가 다른 객체를 레퍼런스로 가지고 있다면
해당 객체까지 SELECT 쿼리가 나갈 것이다.
public class Member {
private Long id;
private Team team;
}
public class Team {
}
public static void Main(){
Member member = new Member();
member.setId(1L);
EntityManager.persist(member);
EntityManager.flush();
EntityManager.clear();
var findMember = EntityManager.find(Member.class, member.getId());
// MEMBER TABLE과 TEAM TABLE SELECT 쿼리 나감
}
만약 findMember를 find()로 찾아오고 아무 접근도 하지 않는다면(물론 그럴일은 없겠지만)
불필요한 SELECT 쿼리가 나간것이다.
이때 실제 객체를 가져오는 find() 메소드 말고, getReference()라는 메소드도 있는데
getReference()는 프록시 객체를 가져온다
public static void Main(){
Member member = new Member();
member.setId(1L);
EntityManager.persist(member);
EntityManager.flush();
EntityManager.clear();
var findMember = EntityManager.getReference(Member.class, member.getId());
// 아무런 쿼리가 나가지 않음
print(findMember.getId());
// MEMBER TABLE과 TEAM TABLE SELECT 쿼리 나감
}
프록시 객체만 가져오고 실제 DB에 접근을 하지 않는것을 볼 수 있다.
그 이후 getReference() 메소드로 가져온 객체가 사용될때 그때서야 DB에서 조회를 하는것을 볼 수 있다
이를 LazyLoading 기법이라 하며 싱글톤 패턴을 구현할때 종종 쓰이기도 한다
가짜 클래스인 프록시는 진짜 클래스를 레퍼런스로 가지고 있어 아마 다음과 같이 구현되어있을것이다(뇌피셜임)
public class Proxy_Entity extends Entity {
Entity target;
@override
public Long getId() {
if(target == null){
// target을 실제 DB에서 가져온다
// Lazy Loading
}
target.getId();
}
}
public class Entity {
public Long getId() {
//
}
}
아래 그림을 보면 프록시 객체가 어떻게 사용되는지 더 명확하게 알 수 있다
Member member = em.getReference(Member.class, “id”);
member.getName();
프록시 객체의 특징으로는
- 처음 사용될 때 한번만 초기화 된다
- 프록시 객체가 초기화 될 때, 원본 엔티티로 바뀌는것이 아닌 프록시 그대로 존재하게 된다
- 원본 엔티티를 상속받는다 따라서
- 프록시 객체와 원본을 == 연산자로 타입 비교하면 false가 나온다
- JPA의 모든 객체는 무조건 instanceof 연산자로 타입 비교를 해야한다
void checkMember(Member m1, Member m2){ // 요게 사실 프록시 객체일수도 있음
if(m1.getClass() == m2.getClass()) // 불확실함
if(m1 instanceof Member && m2 instanceof Member) // 확실함
}
- Persist Context에 찾는 엔티티(프록시가 아닌 원본 엔티티)가 이미 있으면, getReference() 메소드는 더 이상 프록시 객체가 아닌 원본 엔티티를 내뱉는다
- 프록시 초기화는 Persist Context를 통해 진행되므로, Persist Context가 close 되거나 clear 된 이후 초기화를 하게 되면 오류를 발생시킨다
- EntityManager.getPersistenceUnitUtil().isLoaded(Object entity) 메소드로 해당 프록시가 초기화 되었는지 확인할 수 있다.
- org.hibernate.Hibernate.initialize(entity) 메소드로 프록시를 강제로 초기화 할 수 있다
- 원래는 사용될 때 초기화 되도록 설정 되어있음
지연 로딩
다시 예제를 보자
public class Member {
private Long id;
private Team team;
}
public class Team {
}
public static void Main(){
Member member = new Member();
member.setId(1L);
EntityManager.persist(member);
EntityManager.flush();
EntityManager.clear();
var findMember = EntityManager.find(Member.class, member.getId());
// MEMBER TABLE과 TEAM TABLE SELECT 쿼리 나감
}
이때 findMember를 가져온 이후, Team에 접근할 일이 많다면 상관 없지만
접근을 아예 하지 않거나 빈도가 적다면??
SELECT TEAM 쿼리는 비효율적인 쿼리가 된다
SELECT TEAM 쿼리는 실제 Member.getTeam()이 호출될 때 나가게 하고 싶다면
다음과 같이 지연 로딩(Lazy Loading) 전략을 사용하면 된다
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
fetch 타입을 Lazy로만 설정해주면 끝!
즉시 로딩
지연 로딩과는 반대로 사용 및 조회 시점이 아닌 생성 시점에 한번에 가져오는 전략이다
따라서 프록시 객체를 가져오는것이 아닌 SELECT 쿼리로 실제 원본 엔티티 객체를 가져오게 된다
실무에서는 절대 사용하지 않는다
즉시 로딩 전략을 쓰면 JPQL에서는 파라미터로 넣은 Query를 그대로 DB에 보내기 때문에
Member에서 Team을 조회하지 않아도 SELECT 쿼리가 두번 나간다
반면 지연 로딩 전략을 쓰면 Team을 원본 엔티티가 아닌 프록시로 가져오기 때문에
SELECT 쿼리가 한번만 나간다. 물론 Team을 사용 및 조회하면 그때는 쿼리가 한번 더 나가겠지만!
@ManyToOne
@OneToOne
이 두가지는 기본 전략이 EAGER이기 때문에 LAZY로 바꿔줘야 한다
@OneToMany
@ManyToMany
이 두가지는 기본 전략이 LAZY이다
CASCADE(영속성 전이)
Cascade 기능은 엔티티 매핑과는 전혀 상관없이, Persist할때 엔티티들간의 관계를 맺어주는 기능이다
ex) 부모 엔티티를 persist 할 때, 자식 엔티티도 persist
@Entity
public class Parent {
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
List<Child> children;
}
@Entity
public class Child {
@ManyToOne
Parent parent;
}
public static void Main(){
Parent parent = new Parent();
Child child = new Child();
parent.getChildren().add(child);
child.setParent(parent);
EntityManager.persist(parent);
// EntityManager.persist(child); Cascade 되기 때문에 해줄 필요가 없음
}
Cascade 전략의 종류는 다음과 같다
- ALL : 모두 적용
- PERSIST : 영속
- REMOVE : 삭제만 적용
Cascade는 소유자가 위와 같이 단일 소유자일때만 사용하는것이 좋다.
즉, Child를 바꾸는 클래스가 Parent 말고도 더 있으면 사용 권장 x
고아 객체
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제해주는 기능이다
@Entity
public class Parent {
@OneToMany(mappedBy = "parent", orphanRemoval = true) // 기본이 false이다
List<Child> children;
}
@Entity
public class Child {
@ManyToOne
Parent parent;
}
public static void Main(){
Parent parent = new Parent();
Child child = new Child();
parent.getChildren().add(child);
child.setParent(parent);
EntityManager.persist(parent);
EntityManager.persist(child);
var findParent = EntityManager.find(Parent.class, id);
parent.getChildren().remove(0);
// orphanRemoval이 true이므로 실제 DELETE 쿼리가 나간다
}
Cascade와 마찬가지로 소유자가 단일 소유자일 때 사용하는것이 좋다
출처 : https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com
'Back-End > JPA' 카테고리의 다른 글
[JPA] 7. 값 타입 (0) | 2022.08.12 |
---|---|
[JPA] 5. 상속관계 Mapping & Mapped Superclass (0) | 2022.08.03 |
[JPA] 4. Entity간의 연관관계 Mapping (0) | 2022.07.30 |
[JPA] 3. Entity Mapping (0) | 2022.07.21 |
[JPA] 2. Persistent에 대한 이해 (0) | 2022.07.20 |