// RealSubject, Proxy가 공유하는 인터페이스
public interface Subject{
void request();
}
// RealSubject 자기가 할 일만 함. SRP
public class RealSubject implements Subject{
public void request(){
//
}
}
// RealSubject를 호출하기 전후로 추가작업
public class Proxy implemnts Subject{
private RealSubject realSubject // inject;
public void request(){
// do something
realSubject.request();
// do something
}
}
Hibernate는 후자. Hibernate 5.3 부터는 ByteBuddy를 사용하고 이전에는 CGLib을 사용했다.
생성된 프록시 객체는 원래 객체의 메서드 호출을 가로채서, entity 객체가 초기화 되었는지 아닌지를 확인하고 아니면 DB에 질의한 다음 원래 메서를 호출한다. 이 과정이 Hibernate Session없이 수행되면 LazyInitializationException이 발생한다.
지연로딩 (lazy loading)
실제로 그 엔티티의 값이 필요할때 데이터베이스에서 조회한다.
Member(N) Team(1) 관계일때, memger.getTeam().getName() 을 할 때까지 Team 엔티티를 DB에서 조회하지 않는다
// Member.java
@Getter
@Entity
@NoArgsConstructor
@ToString
public class Member {
@Id
@Column(name = "member_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
public Member(String name) {
this.name = name;
}
public Member(String name, Team team) {
this.name = name;
this.team = team;
}
}
// Team.java
@Getter
@Entity
@NoArgsConstructor
@ToString
public class Team {
@Id
@Column(name = "team_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Team(String name) {
this.name = name;
}
}
@SpringBootTest
public class EntityTest {
@Autowired
MemberRepository memberRepository;
@Autowired
TeamRepository teamRepository;
@PersistenceContext
EntityManager entityManager;
Long teamId;
@BeforeEach
public void setUp() {
Team team = new Team("team-0");
Team savedTeam = teamRepository.save(team);
List<Member> memberList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
memberList.add(new Member("member" + i, team));
}
memberRepository.saveAll(memberList);
teamId = savedTeam.getId();
}
@Test
@Transactional
@Commit
public void lazyLoading_getReferenceTest(){
entityManager.clear();
Member byId = memberRepository.findById(1L).get();
assertThat(entityManager.getEntityManagerFactory()
.getPersistenceUnitUtil().isLoaded(byId)).isTrue();
// 프록시 객체
assertThat(entityManager.getEntityManagerFactory()
.getPersistenceUnitUtil().isLoaded(byId.getTeam())).isFalse();
// 실제로 조회
byId.getTeam().getName();
assertThat(entityManager.getEntityManagerFactory()
.getPersistenceUnitUtil().isLoaded(byId.getTeam())).isTrue();
}
}
Member member = em.find(Member.class, "member1"); // entity instance
Member member = em.getReference(Member.class, "member1"); // proxy instance
프록시 객체의 초기화 과정
public class MemberProxy extends Member{
Member target = null;
public String getName(){
// 2. 초기화요청
// 3. DB 조회
// 4. 없으면 entity 생성 및 보관 -> proxy에서 실제 엔티티를 참조해서 값을 가져올 수 있다.
}
}
persistent context에 찾는 엔티티가 이미 존재하면 데이터 베이스를 조회 할 필요 없다. em.getReference 호출해도 프록시가 아닌 실제 엔티티가 반환된다.
프록시 초기화는 persistent context의 도움을 받아야된다. em.close()로 준영속 상태에 있을때는 프록시를 초기화 할 수 없다.
엔티티를 프록시로 조회할 때 PK를 파라미터로 전달한다. 접근방식을 프로퍼티(@Access(AccessType.Property) )로 한 경우, 이 식별자 값이 저장되어있기 때문에 get식별자() 메서드를 호출해도 프록시 객체가 초기화되지 않는다. 접근방식이 필드인경우, get식별자() 메서드가 어떤일을 하는 메서드인지 모르기때문에 프록시객체를 초기화한다.
Team team = em.getReference(Team.class, "team1") // team1이 식별자
team.getId(); // getId()는 식별자를 반환하면 됨. 프록시객체 초기화 안됨
즉시로딩/지연로딩 비교
즉시로딩, 지연로딩을 사용할거냐는 어플리케이션 특성에 따라서
즉시로딩
엔티티를 조회할 때 연관 엔티티도 같이 조회. 조인쿼리 실행됨
컬렉션을 하나이상 즉시로딩하는 것은 권장하지 않는다.
fk not null 제약 없이 inner join 하면 항상 빠지는게 있는거 아니가? 1:N, N:1이 무슨상관인지..?
@ManyToOne(fetch=FetchType.EAGER)
select member0_.member_id as member_i1_0_0_,
member0_.name as name2_0_0_,
member0_.team_id as team_id3_0_0_,
team1_.team_id as team_id1_1_1_,
team1_.name as name2_1_1_
from member member0_ left outer join team team1_
on member0_.team_id=team1_.team_id where member0_.member_id=?
지연로딩
연관된 엔티티를 실제 사용할 때 조회. 독립적인 select 쿼리
@ManyToOne(fetch=FetchType.LAZY)
// Member
select member0_.member_id as member_i1_0_0_, member0_.name as name2_0_0_, member0_.team_id as team_id3_0_0_ from member member0_ where member0_.member_id=?
// Member의 연관객체. 실제 사용할 때 아래 쿼리 실행
select team0_.team_id as team_id1_1_0_, team0_.name as name2_1_0_ from team team0_ where team0_.team_id=?
컬렉션 래퍼
hibernate는 엔티티를 영속상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 hibernate가 제공하는 내장 컬렉션으로 변경한다.
일반 엔티티 → 프록시 객체가 지연로딩을 담당. 컬렉션의 경우, 컬렉션 래퍼가 지연로딩을 담당.
Member member = em.find(Member.class, "member1");
List<Order> orders = member.getOrders();
System.out.println(orders.getClass().getName());
// org.hibernate.collection.internal.PersistentBag
JPA 기본 fetch 전략
디폴트 설정값
ManyToOne, OneToOne - eager // 연관된 엔티티가 1개면 eager
OneToMany, ManyToMany - lazy // 연관된 엔티티가 N개면 lazy
모든 연관관계에 lazy를 사용하고. 어플리케이션 개발이 완료됐을때 eager로 바꿔보고 최적화.
영속성전이 (transitive persistence)
특정 엔티티를 영속상태로 만들 때 연관된 엔티티도 함께 영속상태로 만들고 싶을 때
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티가 영속상태여야 한다.
persist Parent → persist Child(o)
persist Child → persist Parent(x)
설정 방법 @OneToMany(..., cascade=Cascade.PERSIST) 리스트로 여러개 넣어줄 수 있음. cascade={PERSIST, MERGE}
Cascade 종류
앞장에서 소개된 persistent context 내 엔티티의 상태와 관련지어서 보면 된다. 부모 엔티티가 해당상태로 되었을때 자식 엔티티도 따라서 감.
Proxy Pattern
지연로딩 (lazy loading)
Member(N) Team(1) 관계일때, memger.getTeam().getName() 을 할 때까지 Team 엔티티를 DB에서 조회하지 않는다
@Access(AccessType.Property)
)로 한 경우, 이 식별자 값이 저장되어있기 때문에get식별자()
메서드를 호출해도 프록시 객체가 초기화되지 않는다. 접근방식이 필드인경우,get식별자()
메서드가 어떤일을 하는 메서드인지 모르기때문에 프록시객체를 초기화한다.즉시로딩/지연로딩 비교
즉시로딩, 지연로딩을 사용할거냐는 어플리케이션 특성에 따라서
즉시로딩
fk not null 제약 없이 inner join 하면 항상 빠지는게 있는거 아니가? 1:N, N:1이 무슨상관인지..?지연로딩
연관된 엔티티를 실제 사용할 때 조회. 독립적인 select 쿼리
컬렉션 래퍼
일반 엔티티 → 프록시 객체가 지연로딩을 담당. 컬렉션의 경우, 컬렉션 래퍼가 지연로딩을 담당.
JPA 기본 fetch 전략
디폴트 설정값
모든 연관관계에 lazy를 사용하고. 어플리케이션 개발이 완료됐을때 eager로 바꿔보고 최적화.
영속성전이 (transitive persistence)
@OneToMany(..., cascade=Cascade.PERSIST)
리스트로 여러개 넣어줄 수 있음. cascade={PERSIST, MERGE}Cascade 종류
앞장에서 소개된 persistent context 내 엔티티의 상태와 관련지어서 보면 된다. 부모 엔티티가 해당상태로 되었을때 자식 엔티티도 따라서 감.
orphanRemoval
DDD의 aggregate root