2022. 7. 6. 11:50ㆍLanguage`/JPA
<예시>
선수 Entity와 팀 Entity가 존재하고 서로 다대일 관계이다.
Player (N)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "player")
public class Player {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "player_id")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
Team (1)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "team")
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
private String name;
}
그리고 DB에는 다음과 같이 데이터가 쌓여있다
비즈니스 로직 1) 선수를 조회할 때 반드시 해당 선수가 속한 팀도 같이 조회
비즈니스 로직 2) 선수를 조회할 때는 선수만 조회
일단 비즈니스 로직 1)을 먼저 살펴보자
static void printPlayerAndTeam(Long id){
Player player = em.find(Player.class, id);
System.out.println("회원 정보 = " + player);
System.out.println("소속팀 정보 = " + player.getTeam());
}
printPlayerAndTeam(1L);
Hibernate:
select
player0_.player_id as player_i1_0_0_,
player0_.name as name2_0_0_,
player0_.team_id as team_id3_0_0_,
team1_.team_id as team_id1_1_1_,
team1_.name as name2_1_1_
from
player player0_
left outer join
team team1_
on player0_.team_id=team1_.team_id
where
player0_.player_id=?
회원 정보 = Member{id=1, name='member1'}
소속팀 정보 = Team{id=1, name='teamA'}
query를 잘 보면 회원을 찾아올때 연관된 팀도 같이 조회한 것을 확인할 수 있다
그러면 이제 비즈니스 로직 2)를 살펴보자
static void printPlayer(Long id){
Player player = em.find(Player.class, id);
System.out.println("회원 정보 = " + player);
}
printPlayer(1L);
Hibernate:
select
player0_.player_id as player_i1_0_0_,
player0_.name as name2_0_0_,
player0_.team_id as team_id3_0_0_,
team1_.team_id as team_id1_1_1_,
team1_.name as name2_1_1_
from
player player0_
left outer join
team team1_
on player0_.team_id=team1_.team_id
where
player0_.player_id=?
회원 정보 = Member{id=1, name='member1'}
분명 비즈니스 로직 2)에서는 회원만 조회하기로 약속했는데 query를 보면 굳이 조회하지 않아도 되는 "팀"도 같이 조회가 됨을 확인할 수 있다
>> JPA에서는 이러한 문제들을 해결하려고 "엔티티가 실제 사용될 때까지 조회를 지연"을 하기 위해 "지연 로딩"이라는 방법을 제공한다
프록시
JPA에서 기본적으로 엔티티를 조회할 때 "em.find()"를 통해서 조회한다
em.find는 영속성 컨텍스트에 조회하려는 엔티티가 있으면 1차 캐시에서 바로 return하지만, 만약 영속성 컨텍스트에 존재하지 않는다면 "DB"를 직접 조회한다
따라서 이렇게 em.find()를 통해서 엔티티를 직접 조회하게 되면 조회한 엔티티를 실제로 사용하든 사용하지 않든 결국 DB로부터 직접 조회하게 되는 것이다
따라서 엔티티를 실제 사용하는 시점까지 DB 조회를 미루고 싶다면 em.find()가 아닌 "em.getReference()"를 사용해야 한다
em.getReference()를 통해서 엔티티를 조회하려고 하면 JPA에서는 DB를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다
>> 그대신 DB 조회를 "지연"시키는 프록시 엔티티 객체를 return해온다
프록시 객체 구조
Team referenceTeam = em.getReference(Team.class, 1L);
System.out.println(referenceTeam.getClass());
Player referencePlayer = em.getReference(Player.class, 1L);
System.out.println(referencePlayer.getClass());
조회한 "Team Proxy"의 구조는 다음과 같다
만들어진 프록시 객체는 "실제 Team 객체를 상속"받아서 만들어지기 때문에 외부에서 봤을 때는 동일한 모습을 지니고 있기 때문에 실제인지 가짜인지 구분할 필요가 없고 그냥 사용하면 된다
getReference()를 호출하는 시점에는 query가 날라가지 않지만 getReference를 통해서 받은 프록시 객체의 필드값에 접근할 때 query가 날라가게 된다
- 위의 프록시 객체에 대해서 getName()처럼 실제로 값을 사용할 때 그제서야 DB를 조회해서 실제 Entity를 생성하고 "target이 실제 Entity를 참조"하도록 만든다
프록시 객체 초기화
Team referenceTeam = em.getReference(Team.class, 1L);
String referenceTeamName = referenceTeam.getName();
이렇게 프록시 객체의 실제값에 접근을 하게되면 프록시 초기화 과정이 일어나게 되고 이 초기화 과정을 알아보자
1. 프록시 객체에 대한 getName() 호출
2. 프록시 객체는 "영속성 컨텍스트"에 초기화 요청
- 프록시 객체는 실제 Entity를 참조하고 있지 않는 상태라면 영속성 컨텍스트에게 "실제 Entity를 생성해주세요"라고 요청하는데 이것을 초기화라고 한다
3. 영속성 컨텍스트는 프록시 객체를 생성할 때 받았던 "PK"를 통해서 DB를 조회해서 실제 Entity를 생성한다
4. 프록시 객체는 생성된 실제 Entity를 "target 변수"를 통해서 참조한다
5. 프록시 객체에 대한 getName()호출은 결국 target.getName()을 통해서 이루어진다
>> 이를 코드와 실행된 결과를 통해서 확인해보자
System.out.println("======== 프록시 객체 생성 ========");
Team referenceTeam = em.getReference(Team.class, 1L);
System.out.println("생성된 프록시 : " + referenceTeam.getClass());
System.out.println("======== 실제 Entity 생성 후 프록시 target이 참조 ========");
System.out.println("팀 이름 = " + referenceTeam.getName()); // 이 부분에서 초기화 과정이 발생
======== 프록시 객체 생성 ========
생성된 프록시 : class proxy.domain.Team$HibernateProxy$UuXea3bJ
======== 실제 Entity 생성 후 프록시 target이 참조 ========
Hibernate:
select
team0_.team_id as team_id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.team_id=?
팀 이름 = teamA
프록시 특징
1. 프록시 객체는 "처음 사용될 때 1번만 초기화"된다
System.out.println("======== 프록시 객체 생성 ========");
Team referenceTeam = em.getReference(Team.class, 1L);
System.out.println("생성된 프록시 : " + referenceTeam.getClass());
System.out.println("======== 첫번째 사용 ========");
System.out.println(referenceTeam.getName()); // 이 부분에서 초기화 과정이 발생
System.out.println("======== 두번째 사용 ========");
System.out.println(referenceTeam.getName());
System.out.println("======== 세번째 사용 ========");
System.out.println(referenceTeam.getName());
======== 프록시 객체 생성 ========
생성된 프록시 : class proxy.domain.Team$HibernateProxy$6iYQVcZu
======== 첫번째 사용 ========
Hibernate:
select
team0_.team_id as team_id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.team_id=?
teamA
======== 두번째 사용 ========
teamA
======== 세번째 사용 ========
teamA
결과로 알수있듯이 프록시 객체는 처음 사용할 때만 초기화 과정을 수행하고 그 다음부터는 사용할 때 초기화된 terget을 이용해서 사용한다
2. 프록시 객체를 초기화한다고해서 프록시 객체 → 실제 Entity로 변하는 것은 "절대 아니다"
프록시 객체를 초기화하면 프록시 객체 내부의 target이 실제 Entity를 "참조"하는 것이지 프록시 객체가 실제 Entity로 변하는 것은 아니다
3. 프록시 객체는 "실제 Entity를 상속받은 객체"이므로 타입 체크시 주의해야 한다
프록시 객체에 대한 타입을 체크할 때는 "=="이 아닌 "instanceof"로 체크해야 한다
Team referenceTeam = em.getReference(Team.class, 1L);
System.out.println("referenceTeam.getClass() == Team.class ? " + (referenceTeam.getClass() == Team.class));
System.out.println("referenceTeam.getClass() instanceof Team ? " + (referenceTeam instanceof Team));
referenceTeam.getClass() == Team.class ? false
referenceTeam.getClass() instanceof Team.class ? true
4. getReference()를 호출했는데 이미 호출하려는 Entity가 영속성 컨텍스트에 존재한다면 프록시 객체가 아닌 실제 Entity가 반환된다
(1) em.find() 후 em.getReference()
Team findTeam = em.find(Team.class, 1L);
System.out.println("findTeam = " + findTeam.getClass());
Team referenceTeam = em.getReference(Team.class, 1L);
System.out.println("referenceTeam = " + referenceTeam.getClass());
---------------
select ~~~
---------------
findTeam = class proxy.domain.Team
referenceTeam = class proxy.domain.Team
이미 em.find()를 통해서 영속성 컨텍스트에 집어넣었기 때문에 em.getReference()를 통해서 찾아도 영속성 컨텍스트에 존재하는 실제 Entity가 반환된다
(2) em.getReference() 후 em.getReference()
Team referenceTeam1 = em.getReference(Team.class, 1L);
System.out.println("referenceTeam1 = " + referenceTeam1.getClass());
Team referenceTeam2 = em.getReference(Team.class, 1L);
System.out.println("referenceTeam2 = " + referenceTeam2.getClass());
referenceTeam1 = class proxy.domain.Team$HibernateProxy$ep3hFkwS
referenceTeam2 = class proxy.domain.Team$HibernateProxy$ep3hFkwS
당연히 둘다 동일한 프록시 객체이다
(3) em.getReference() 후 em.find()
Team referenceTeam = em.getReference(Team.class, 1L);
System.out.println("referenceTeam = " + referenceTeam.getClass());
Team findTeam = em.find(Team.class, 1L);
System.out.println("findTeam = " + findTeam.getClass());
referenceTeam = class proxy.domain.Team$HibernateProxy$jzq0mPh0
---------------
select ~~~
---------------
findTeam = class proxy.domain.Team$HibernateProxy$jzq0mPh0
여기서 드는 의문점은 "em.find()를 통해서 실제로 조회하는 것으로 생각했는데 왜 프록시가 반환되는 걸까?"
>> JPA에서는 "엔티티의 동일성"을 보장하기 위해서 find로 조회함에도 불구하고 앞에서 이미 프록시 객체를 조회했기 때문에 em.find()를 호출해도 프록시 객체가 반환된다