2022. 7. 6. 12:43ㆍLanguage`/JPA
앞서 배운 "프록시"개념은 주로 연관된 엔티티를 "지연 로딩"할 때 사용한다
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
@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;
}
이전 프록시 포스팅에서 발생했던 문제는 다음과 같다
비즈니스 로직 1)에서는 애초에 로직자체가 회원을 조회할 때 팀도 같이 조회하자는 로직이였다
하지만 비즈니스 로직 2)에서는 회원만 조회하기로 하였는데 원하지 않는 "팀"도 같이 조회가 됨을 확인할 수 있었다
이렇게 두 로직이 서로 대립할 때 회원 엔티티와 연관된 "팀 엔티티"에 대한 고민
1) 회원 엔티티를 조회할 때 DB에서 함께 조회하는 것이 맞을까 (즉시 로딩)
2) 아니면 팀 엔티티를 실제 사용하는 시점에 DB에서 조회하는게 맞을까 (지연 로딩)
1)의 경우를 "즉시 로딩 전략"이라고 하고, 2)의 경우를 "지연 로딩 전략"이라고 한다
즉시 로딩
즉시 로딩 전략을 사용하려면 연관관계 애노테이션의 fetch 속성을 FetchType.EAGER로 설정하면 된다
@~~~ToOne은 기본 전략이 EAGER(즉시 로딩)이고, @~~ToMany는 기본 전략이 LAZY(지연 로딩)이다
Example)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
현재 Member는 "즉시 로딩 전략"을 이용해서 Member를 조회할 때 연관된 Team도 그 시점에 전부 로딩하기로 하였다
Member findMember = em.find(Member.class, 1L);
Team findMemberTeam = findMember.getTeam();
System.out.println("선수 = " + findMember);
System.out.println("소속 팀 = " + findMemberTeam);
System.out.println("\n===== 프록시 여부 확인 =====");
System.out.println(findMember.getClass());
System.out.println(findMemberTeam.getClass());
Hibernate:
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=?
선수 = Member{id=1, name='member1'}
소속 팀 = Team{id=1, name='teamA'}
===== 프록시 여부 확인 =====
class proxy.domain.Member
class proxy.domain.Team
즉시 로딩 전략을 사용하고 프록시 여부를 확인해보았는데 Member는 당연히 실제 Entity이고, 즉시 로딩에 의해 조회된 Team 또한 실제 Entity이다
>> "즉시 로딩 전략"을 활용하면 연관된 Entity도 DB에서 조회해서 실제 Entity가 반환된다
대부분의 JPA 구현체는 "즉시 로딩을 최적화"하기 위해서 가능하면 조인 쿼리를 사용한다
>> 위의 예제에서도 Member와 Team간의 left outer join을 통해서 쿼리 1번으로 연관된 두 엔티티를 모두 조회하였다
※ 즉시로딩 & NULL 제약 조건
위의 조인을 잘 보면 "left outer join" : 외부조인을 통해서 Member과 Team이 조인을 하고 있다
현재 Member Table에서는 Team 컬럼에 대한 nullable을 허용하고 있는 상태이다
>> 따라서 Player중에 team이 없는 Player도 존재한다는 의미이다
그렇기 때문에 이 Member에 대한 instance도 가져와야 하기 때문에 내부 조인이 아닌 외부 조인을 하는 것이다
그런데 여기서 당연히 외부 조인보다는 내부 조인이 성능적 이점이 더 크다
>> 따라서 Team에 대한 nullable을 허용하지 않는다면 외부 조인이 아닌 내부 조인이 이루어질 것이다
(1) nullable = true
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
...
...
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
Hibernate:
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=?
(2) nullable = false
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
...
...
@ManyToOne
@JoinColumn(name = "team_id", nullable = false)
private Team team;
}
Hibernate:
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_
inner join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.member_id=?
여기서 @JoinColumn에서 nullable = false를 통해서 내부 조인을 사용하도록 할 수도 있고, 연관관계 애노테이션에 "optional = false"를 사용해도 내부 조인을 사용하도록 할 수 있다
(3) optional = false
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
...
...
@ManyToOne(optional = false)
@JoinColumn(name = "team_id")
private Team team;
}
Hibernate:
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_
inner join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.member_id=?
따라서 JPA에서의 [외부조인 / 내부조인] 선택 전략은 다음 관계에 의해서 결정된다
→ 선택적 관계 = 외부 조인
→ 필수적 관계 = 내부 조인
<FetchType.EAGER + 조인 전략>
@xxxToOne
- optional = false : 내부 조인
- optional = true : 외부 조인
@xxxToMany
- optional = false : 외부 조인
- optional = true : 외부 조인
@xxxToMany의 경우 컬렉션으로 양방향 매핑을 한 경우이다.
컬렉션 즉시로딩은 무조건 "외부 조인"을 사용한다
왜냐하면 반드시 조회가 되어야 하는 "One"의 경우 만약 컬렉션 내부에 element가 하나도 없다면 조회가 되지 않는 문제가 발생할 수 있다.
>> 그래서 웬만하면 @xxxToMany의 기본 로딩 전략인 LAZY는 변경하지말고 무조건 LAZY로 사용해야 한다
지연 로딩
지연 로딩을 사용하려면 fetch 속성을 FetchType.LAZY로 설정하면 된다
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
Member findMember = em.find(Member.class, 1L);
Team findMemberTeam = findMember.getTeam();
System.out.println("회원 : " + findMember);
System.out.println("회원 소속 팀 : " + findMemberTeam); // 여기서 프록시 객체가 실제 Team Entity를 가리키게 된다
System.out.println("\n===== 프록시 여부 확인 =====");
System.out.println(findMember.getClass());
System.out.println(findMemberTeam.getClass());
Hibernate:
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{id=1, name='member1'}
Hibernate:
select
team0_.team_id as team_id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.team_id=?
회원 소속 팀 : Team{id=1, name='teamA'}
===== 프록시 여부 확인 =====
class proxy.domain.Member
class proxy.domain.Team$HibernateProxy$I82eho2L
LAZY전략은 실제 사용할 때 DB에서 조회해오는 것이기 때문에 Team을 실제 사용하는 시점에 DB로 select query를 날려서 team을 찾아온다
그리고 프록시 여부를 확인해보면 Member는 당연히 실제 Entity가 반환되고, 여기서 실제 Member Entity를 반환할 때 연관된 Team Entity는 "지연 로딩 전략"을 사용하기 때문에 Team은 "프록시 객체"로 존재하게 된다
연관된 엔티티가 이미 영속성 컨텍스트에 존재한다면??
연관된 Team Entity가 "이미 영속성 컨텍스트에 존재"한다면 프록시 객체를 사용할 이유가 없다
따라서 영속성 컨텍스트에 연관된 엔티티가 이미 존재한다면 "지연 로딩 전략"을 사용해도 이미 있기 때문에 실제 Entity로 사용할 수 있다
Team findTeam = em.find(Team.class, 1L); // findMember의 팀을 미리 조회해서 실제 Team Entity를 영속성 컨텍스트에 보관
Member findMember = em.find(Member.class, 1L);
Team findMemberTeam = findMember.getTeam(); // 이미 영속성 컨텍스트에 존재하기 때문에 프록시가 아닌 실제 Entity
System.out.println("회원 : " + findMember);
System.out.println("회원 소속 팀 : " + findMemberTeam);
System.out.println("\n===== 프록시 여부 확인 =====");
System.out.println(findMember.getClass());
System.out.println(findMemberTeam.getClass());
System.out.println("findTeam == findMemberTeam ? " + (findTeam.getClass() == findMemberTeam.getClass()));
Hibernate:
select
team0_.team_id as team_id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.team_id=?
Hibernate:
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{id=1, name='member1'}
회원 소속 팀 : Team{id=1, name='teamA'}
===== 프록시 여부 확인 =====
class proxy.domain.Member
class proxy.domain.Team
findTeam == findMemberTeam ? true