[김영한 - JPA 기본] #8 프록시와 연관관계 관리
프록시 Proxy
그래서 프록시 (Proxy) 가 대체 머야?! ㅁㅇㅁㅇ
필자가 생각하는 프록시는 크게 두가지 효용을 가진다
- 캐싱
- 공통처리
1. 캐싱
캐싱은 진짜 real 객체를 호출하기엔 비용이 크게 드는 것이다 그래서 일단 정말로 필요한 순간까지 미루다가
필요 순간이 오면 진짜 객체를 동적으로 생성 혹은 받아와서 그때서야 주는 것이다. 이때 생성된 진짜 객체는 내부적으로 static 등의 필드로 저장되어 관리된다 (캐싱
)
2. 공통처리
코드를 작성하다보면 핵심 비즈니스 로직 이외의 부가적인 기능 (로깅, 트랜잭션)이 필요할 떄가 있다 이때 이 코드들은 거의 모든 비즈니스 핵심 코드에서 삽입이 되며 코드를 읽거나 유지보수하는데 어려움이 생긴다 이때 프록시 패턴을 이용하면 좋다 ! 공통처리 코드를 만들어 놓고 비즈니스 객체의 메서드 호출을 그안에 삽입하는 것이다 이것은 AOP
의 근간 원리가 된다
프록시의 정의 및 특징
- 실제 클래스를 상속받아서 만들어진 가짜 클래스이다
- 진짜 클래스는
Entity target = null
등으로 받아오는 듯하다
- 진짜 클래스는
- 사용할 때는 진짜/가짜 유무를 구분하지 않고 사용하면 된다 한다 (이론상)
- 실제 객체의 참조를 보관한다
getReference()
를 통해서 프록시 객체를 초기화를 한다- 처음 사용할 때 한 번만 초기화 한다
- 초기화할 때 실제 엔티티로 바뀌는 것은 아니다
- 초기화되면 프록시를 통해 실제 엔티티로 접근이 가능하다
- 프록시 객체는 원본 엔티티를 상속받기 때문에 타입체크시
==
대신에instance of
를 사용하여 비교하자 - 영속 컨텍스트의 도움을 받을 수 없는 준영속(detached) 상태일 때, 프록시를 초기화하면 예외 발생
프록시 관련 API들
- 초기화 여부 확인
PersistenceUnitUtil persistenceUnitUtil = emf.getPersistenceUnitUtil();
- 강제 초기화
Hibernate.initialize(entity)
- jpa 표준은 강제 초기화가 없기 때문에
- member.getName() 등을 호출하여 초기화 가능하다..
강의에서 보여준 실험 코드들
핵심 전제 : 조회된 객체(진짜, 가짜) 모두 == 연산시
true
가 나와야 한다
find
조회 후getReference
처음부터 진짜 객체가 조회되므로 가짜 객체가 조회될 필요가 없다
getReference
조회 후find
가짜를 조회한 후 find를 통해 조회하더라도 가짜 객체가 나온다 find로 조회한 것이 꼭 원객체임을 보장할 수 없는 것이다
REPEATABLE READ JPA에서는 같은 트랜잭션이라면 같은 객체임을 보장을 해주어야만 한다
프록시 초기화 시점에 대해 실험해본 것들
리스트1 : 초기화 시점 getReference
Team teamA = Team.builder().name("team a").build();
Member userA = Member.builder().name("user a").build();
Member userB = Member.builder().name("user b").build();
teamA.addMembers(userA, userB);
em.persist(teamA);
em.persist(userA);
em.persist(userB);
em.flush();
em.clear();
PersistenceUnitUtil persistenceUnitUtil = emf.getPersistenceUnitUtil();
out.println("----------------------- 실험 START -----------------------");
Team findTeam = em.getReference(Team.class, teamA.getId());
out.println("findTeam.getClass().getName() = " + findTeam.getClass().getName());
boolean loaded = persistenceUnitUtil.isLoaded(findTeam);
out.println("loaded = " + loaded); // (1)
out.println("findTeam.getId() = " + findTeam.getId());
out.println("loaded = " + persistenceUnitUtil.isLoaded(findTeam)); // (2)
out.println("findTeam.getName() = " + findTeam.getName());
out.println("loaded = " + persistenceUnitUtil.isLoaded(findTeam)); // (3)
out.println("----------------------- 실험 END -----------------------");
false -> true로 바뀌는 시점은 (3)
이다! 이곳에서 프록시가 초기화를 한다
id는 이기 가지고 있던 값이기 때문에 초기화를 하지 않지만 name은 없기 떄문에 select쿼리가 나가면서 초기화를 진행한다
리스트2: 초기화 시점: 연관관계 참조 필드: 일에서 다
Team teamA = Team.builder().name("team a").build();
Member userA = Member.builder()
.name("user a")
.age(15)
.build();
Member userB = Member.builder()
.name("user b")
.age(19)
.build();
teamA.addMembers(userA, userB);
em.persist(teamA);
em.flush();
em.clear();
Team findTeam = em.find(Team.class, teamA.getId());
PersistenceUtil pu = Persistence.getPersistenceUtil();
List<Member> findMembers = findTeam.getMembers();
out.println("is findMembers loaded ? " + pu.isLoaded(findMembers));
out.println("#findMembers = " + findMembers.getClass().getName());
Member findMember0 = findMembers.get(0); // #1
out.println("findMember0 = " + findMember0.getClass().getName());
out.println("is findMembers loaded ? " + pu.isLoaded(findMembers));
#1
에서 초기화된다 !
참고글: https://www.nowwatersblog.com/jpa/ch8/8-3
리스트3: 초기화 시점 : 연관관계 참조 필드: 다에서 일
Team teamA = Team.builder().name("team a").build();
Member userA = Member.builder()
.name("user a")
.age(15)
.build();
Member userB = Member.builder()
.name("user b")
.age(19)
.build();
teamA.addMembers(userA, userB);
em.persist(teamA);
em.flush();
em.clear();
PersistenceUtil pu = Persistence.getPersistenceUtil();
Member findMemberA = em.find(Member.class, userA.getId());
Team findTeam = findMemberA.getTeam();
out.println("#1 is findTeam load ? " + pu.isLoaded(findTeam));
out.println("#1 findTeam = " + findTeam.getClass().getName());
findTeam.getId();
out.println("#2 is findTeam load ? " + pu.isLoaded(findTeam));
out.println("#2 findTeam = " + findTeam.getClass().getName());
findTeam.getName(); // #1
out.println("#3 is findTeam load ? " + pu.isLoaded(findTeam));
out.println("#3 findTeam = " + findTeam.getClass().getName());
#1
에서 초기화 된다 !! id는 이미 프록시가 가지고 있지만 name은 가지고 있지 않기 떄문이다 !
select
member0_.id as id1_0_0_,
member0_.age as age2_0_0_,
member0_.name as name3_0_0_,
member0_.TEAM_ID as TEAM_ID4_0_0_
from
Member member0_
where
member0_.id=?
#1 is findTeam load ? false
#1 findTeam = jpql.Team$HibernateProxy$qTcOoyNB
#2 is findTeam load ? false
#2 findTeam = jpql.Team$HibernateProxy$qTcOoyNB
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
#3 is findTeam load ? true
#3 findTeam = jpql.Team$HibernateProxy$qTcOoyNB
리스트3: 초기화시점: 일대일 관계
@Entity
public class Locker {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "MEMBER_ID")
private Member member;
public void mapMember(Member member) {
this.member = member;
member.setLocker(this);
}
}
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne(mappedBy = "member", fetch = FetchType.EAGER)
@Setter
private Locker locker;
}
위처럼 FK
는 Locker에 있다고 가정하였다
따라서 연관관계의 주인은 Locker 이다
Locker -> Member 순서대로 가져올 때
연관관계의 주인으로부터 순차적으로 가져올 때의 상황이다
Member memberA = Member.builder().name("swcho").build();
Locker lockerA = Locker.builder().name("locker a").build();
lockerA.mapMember(memberA);
em.persist(lockerA);
em.persist(memberA);
em.flush();
em.clear();
PersistenceUtil pu = Persistence.getPersistenceUtil();
Locker findLocker = em.find(Locker.class, lockerA.getId());
out.println("findLocker = " + findLocker.getClass().getName());
Member findMember = findLocker.getMember();
out.println("findMember class name : " + findMember.getClass().getName());
out.println("#1 is findMember load ? " + pu.isLoaded(findMember));
out.println("findMember.getId() = " + findMember.getId());
out.println("#2 is findMember load ? " + pu.isLoaded(findMember));
out.println("findMember.getName() = " + findMember.getName());
out.println("#3 is findMember load ? " + pu.isLoaded(findMember));
Hibernate:
select
locker0_.id as id1_0_0_,
locker0_.MEMBER_ID as MEMBER_I3_0_0_,
locker0_.name as name2_0_0_
from
Locker locker0_
where
locker0_.id=?
findLocker = hellojpa.domain.Locker
findMember class name : hellojpa.domain.Member$HibernateProxy$2Uprx5Xe
#1 is findMember load ? false
findMember.getId() = 2
#2 is findMember load ? false
Hibernate:
select
member0_.id as id1_1_0_,
member0_.name as name2_1_0_
from
Member member0_
where
member0_.id=?
15:08 TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - [2]
15:08 TRACE o.h.t.descriptor.sql.BasicExtractor - extracted value ([name2_1_0_] : [VARCHAR]) - [swcho]
findMember.getName() = swcho
#3 is findMember load ? true
Member -> Locker 순서대로 가져올 때
연관관계의 owner가 아닌 곳에서 먼저 가져온다
Member memberA = Member.builder().name("swcho").build();
Locker lockerA = Locker.builder().name("locker a").build();
lockerA.mapMember(memberA);
em.persist(lockerA);
em.persist(memberA);
em.flush();
em.clear();
PersistenceUtil pu = Persistence.getPersistenceUtil();
Member findMember = em.find(Member.class, memberA.getId());
Locker findLocker = findMember.getLocker();
out.println("findLocker class name = " + findLocker.getClass().getName());
out.println("findLocker id = " + findLocker.getId());
out.println("#1 is findLocker Loaded = " + pu.isLoaded(findLocker));
out.println("findLocker name = " + findLocker.getName());
out.println("#2 is findLocker Loaded = " + pu.isLoaded(findLocker));
Hibernate:
select
member0_.id as id1_1_0_,
member0_.name as name2_1_0_
from
Member member0_
where
member0_.id=?
Hibernate:
/* load hellojpa.domain.Locker */ select
locker0_.id as id1_0_0_,
locker0_.MEMBER_ID as MEMBER_I3_0_0_,
locker0_.name as name2_0_0_
from
Locker locker0_
where
locker0_.MEMBER_ID=?
findLocker class name = hellojpa.domain.Locker
findLocker id = 1
#1 is findLocker Loaded = true
findLocker name = locker a
#2 is findLocker Loaded = true
처음부터 select 쿼리가 두 번 나가는 것을 볼 수 있고 처음부터 진짜 객체를 가져온다
참고 https://blog.advenoh.pe.kr/database/JPA-%EC%9D%BC%EB%8C%80%EC%9D%BC-One-To-One-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84/
즉시 로딩과 지연 로딩
핵심 이슈
개념을 이해하는데 도움이 되는 이슈
List<Member> findMemberList = em.createQuery("select m from Member m", Member.class).getResultList();
out.println("findMemberList = " + findMemberList);
위와 같은 JPQL 쿼리는 특별한 설정이 없으면 N+1 문제를 발생시킨다
즉 member객체를 모두 찾은 다음에 각 객체에 대한 team객체를 가져오기 위한 쿼리를 발생한다
내가 작성한 쿼리는 하나(1)지만 member가 N개일 경우 1+N, 즉 N+1 문제를 발생
만일 member가 1000명이고 team이 100명이면 100 + 1
의 쿼리가 발생한다
해결법
class Member {
...
@ManyToOne(fetch = FetchType.LAZY) // default는 EAGER 다 !!
@JoinColumn(name = "TEAM_ID")
private Team team;
위와 같이 LAZY 로딩
을 별도로 설정해준다 !
정리
XToOne 즉 ``@ManyToOne`, @OneToOne 은 즉시로딩을 한다
- 따라서 예상치 못한 쿼리가 나갈 수 있다
JPQL
에서 N+! 문제 발생
- 모든 연관관계에 지연 로딩을 사용할 것 ! (특히 실무에서)
- 실무에서 즉시 로딩을 사용하지 말 것 !
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라!
앞으로 해볼 것들
- 여러 테이블이 묶여 있는 상황에서 즉시, 즉시 로딩을 발생시켜보기 (O)
- 자바로 가짜 DB 클래스, Proxy를 만들고 지연 로딩을 만들어보기
- 기술에 대한 구체적인 상상을 해보는 것 !! 이 목적이다
지연, 즉시 로딩 실험
- 여러 테이블이 묶여 있는 상황에서 즉시 로딩을 발생시켜보기
Locker lockerA = Locker.builder().name("locker A").build();
Member userA = Member.builder().name("user A").build();
Team teamA = Team.builder().name("team A").build();
lockerA.mapMember(userA);
teamA.addMembers(userA);
em.persist(teamA);
em.persist(userA);
em.persist(lockerA);
em.flush();
em.clear();
List<Team> teams = em.createQuery("select t from Team t", Team.class).getResultList();
teams.forEach(out::println);
locker, member , team은 모두 연관관계를 가지는 객체인데, 이때 모두 eager로딩으로 되어 있다고 전제한다
영속성 전이: CASCADE
다대일
관계에서일
의 엔티티를 영속화할 때다
도 같이 하고 싶을 때- 예: 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장.
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
많이 CASCADE의 종류
- ALL: 모두 적용
- PERSIST: 영속
- REMOVE: 삭제
고아 객체
다대일
에서일
의 연관관계가 끊어진다
의 엔티티는 자동 삭제
orphanRemoval = true