프록시
프록시 기초
- em.find 같은 경우 실제 엔티티 객체를 조회한다. 하지만 em.getReference는 데이터 베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다. 여기서 가짜 객체가 뭘까?
- DB에 쿼리를 날리지 않지만 객체가 조회가 된다. 이게 무슨말일까
해당 코드는 member find 메서드가 실행되며 즉시 memberEntity의 모든값을 요청한다
// db에 값을 저장해준다
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member member1 = em.find(Member.class, member.getId());
System.out.println("member1.getUsername() = " + member1.getUsername());
System.out.println("member1.getId() = " + member1.getId());
하지만 getReference 메서드를 사용하면 이미 캐시에 저장되어있는 값인 id는 sout 명령어가 요청되는 순간 호출되지만 username은 해당 메서드가 요청되는 순간 쿼리를 날려 값을 가져온다
Member member1 = em.getReference(Member.class, member.getId());
System.out.println("member1.getId() = " + member1.getId());
System.out.println("member1.getUsername() = " + member1.getUsername());
member를 getClass로 조회해보면
member1 = class hellojpa.Member$HibernateProxy$Iq4tzHLN
반환 타입이 Member가 아닌 뒤에 이상한 값이 붙어있음을 확인할 수 있다.
이걸 바로 프록시 클래스라고 하는데 이것을 바로 가짜 엔티티 객체라고도 한다.
프록시 특징
- 실제 클래스를 상속받아서 만들어진다 (하이버네이트가 내부적으로 처리해줌)
- 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨 (이론상)
- 생김세가 마치 자바의 stack 영역에 참조값을 두는 참조타입을 보는 것 같다.
- 프록시 객체는 실제 객체의 참조(target)를 보관
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출
프록시 객체의 작동 순서는 다음과 같다
client의 getName 호출 → 프록시 객체가 영속성 컨텍스트에 Entity를 요청 → 실제 생성된 Entity에서 해당 메소를 실행
내가 이해한 바로는 이렇다.
자바에서 interface를 만들고 해당 interface를 구현하는 객체를 만들듯이 프록시를 먼저 만들고 해당 프록시가 호출될때 컨텍스트에 Entity를 담아준 후 프록시의 메소드(엔티티의 메소드)를 실행시킨다
이를 정리해보자면
- 프록시 객체는 처음 사용할 때 한번만 초기화
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
- 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크 시 주의해야함 (== 비교실패, 대신 instance of 사용)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference() 를 호출해도 실제 엔티티 반환
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때 프록시를 초기화 하면 문제 발생 (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
즉시 로딩과 지연 로딩
지연 로딩
지연로딩의 경우에는 다음과 같다. 하나의 엔티티만 조회하고 싶은데 조회 명령을 내리면 해당 엔티티와 연관관계에 있는 모든 엔티티를 다 끌어오게 된다. 이는 필요 이상의 리소스를 발생시키는 요인이 될 것이다. 이를 해결하기 위해 JPA 에서는 지연로딩을 지원한다.
지연 로딩을 선언하는 방법은 다음과 같다.
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Member {
@Id @GeneratedValue @Column(name = "member_id")
private Long id;
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
이렇게 어노테이션 옆에 선언해주게 되면 해당 객체는 엔티티가 아닌 프록시 객체로 반환하게 된다. 이는 즉 team 객체가 호출될 때 컨텍스트에 등록된다는 것을 알 수 있다 (똑똑한 JPA)
// db에 값을 저장해준다
Member member1 = new Member();
member1.setUsername("hello");
em.persist(member1);
em.flush();
em.clear();
Member m = em.find(Member.class, member1.getId());
System.out.println("m.getId() = " + m.getId());
해당 코드를 실행시켜 보면 member의 엔티티를 요청하는데 사용되는 쿼리에 team 테이블이 조인되지 않는다. 하지만
Team team = new Team();
team.setName("Team1");
em.persist(team);
Member member1 = new Member();
member1.setUsername("hello");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
Member m = em.find(Member.class, member1.getId());
System.out.println("m.getId() = " + m.getId());
System.out.println("m.getTeam() = " + m.getTeam());
해당 코드를 실행시켜보면 우선 getId를 조회하기 위한 쿼리가 먼저 나가고 sout을 출력한 뒤 team을 가져오기 위한 쿼리가 나간다.
이처럼 지연로딩을 사용해주면 하나의 엔티티와 조인된 엔티티에 해당하는 프록시 객체를 조회해온다.
즉시 로딩
하지만 이와 반대로 MemberEntity와 TeamEntity가 자주 함께 사용된다면 오히려 지연 로딩을 걸어 주는것이 리소스 낭비가 될 수 있다. 이와 같은 상황을 위해 JPA는 즉시 로딩을 지원한다.
즉시 로딩을 설정해주는 방법은 다음과 같다.
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
지연 로딩에서 어노테이션 하나만 바꿔주면 된다
아까 LAZY를 설정해줬을때와는 달리 member를 조회하면 team 객체에 프록시를 반환하는것이 아닌 바로 team의 Entity가 조회가 된다.
즉시로딩의 주의점은 다음과 같다
- 가급적 지연로딩만 사용 (특히 실무에서)
- 즉시로딩을 적용하면 예상하지 못한 SQL이 발생
- 즉시로딩은 JPQL에서 N+1 문제를 일으킨다. !!!!
- @ManyToOne, @OneToMany는 기본이 즉시로딩 → LAZY로 설정
- @OneToMany, @ManyToMany는 기본이 지연로딩
그 냥 쓰 지 마
그래도 즉시로딩을 사용하고 싶다면 fetch 조인을 사용하자
영속성 전이 : CASCADE
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속상태로 만들고 싶을때
- 예 : 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장
실습을 위한 코드
Parent Entity
package hellojpa;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Parent {
@Id @GeneratedValue
@Column(name = "parent_id")
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);
}
}
ChildEntity
package hellojpa;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Child {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
그동안 배워왔던 JPA로 child와 parent를 persist 하는 코드는 다음과 같다
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할때 연관된 다른 entity도 모두 persist되도록 해주는게 cascade이다.
수정된 parentEntity
package hellojpa;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Parent {
@Id @GeneratedValue
@Column(name = "parent_id")
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
}
수정된 코드
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
이처럼 parent의 객체에 포함된 다른 child들도 자동으로 persist 해준다.
해당 기능은 소유주가 하나일때만 사용해야 한다. (완전히 종속적일때만 사용)
고아 객체
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
- orphanRemoval = true
설정 방법은 다음과 같다
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
Parent Entity에 다음과 같은 설정을 추가해 주면 된다.
child 객체를 고아 객체로 임명하였음으로 다음과 같은 코드를 실행시키면 DB에 저장되어있는 child1 객체가 삭제된다.
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());
findParent.getChildList().remove(0);
이 설정 또한 소유주가 하나일때만 사용해야 한다. (완전히 종속적일때만 사용)