N:1 관계에서 연관관계의 주인은 N입니다. 1의 PK를 N이 가지고 있습니다. 따라서, N을 조회할 때 1의 PK, 즉 외래키가 같이 조회됩니다. 이해를 위해 Member(N) : Team(1)의 관계로 설명을 해보겠습니다.
Member에서 Team을 Lazy Loading 하는 상황이라고 가정합시다. Team의 PK를 조회해도 Team에 대한 select문은 실행되지 않습니다. Member를 조회할 때, team_id를 함께 조회해오기 때문입니다.
그렇다면, team_id는 어디서, 어떻게 관리되고 있을까요?
@Entity
@Getter
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
List<Member> members = new ArrayList<>();
@Column(name = "team_name")
public String name;
public Team(String name) {
this.name = name;
}
protected Team() {
}
}
@Entity
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
private Username username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
public Member(String username, Team team) {
this.username = new Username(username);
this.team = team;
}
protected Member() {
}
}
저는 두 가지 중에 하나라고 추측했습니다.
결과는 둘 다 아니었습니다…
먼저 Test를 위한 data를 준비했습니다.
insert into team (team_name) values ('A');
insert into team (team_name) values ('B');
insert into team (team_name) values ('C');
insert into team (team_name) values ('D');
insert into team (team_name) values ('E');
insert into member (name, team_id) values ('chris', 5);
member는 team_id가 5
인 Team과 관계를 맺고 있습니다.
그 후, 아래와 같은 Test를 작성하고 sout이 작성된 line에 Breakpoint를 찍고, Member를 열어보았습니다.
@DataJpaTest
@Sql("classpath:test.sql")
class MemberRepositoryTest {
@Autowired
private MemberRepository memberRepository;
@Test
void myTest() {
Member member = memberRepository.findById(1L)
.orElseThrow(IllegalArgumentException::new);
System.out.println("check it");
}
}
일단 team에는 Proxy 객체가 할당되어 있는 것을 확인할 수 있습니다.
위의 team Proxy를 열어보면 null이 할당되어 있습니다. 저의 추측 1은 틀렸습니다.
그 후, $_hibernate_interceptor
가 의심스러워서 열어보았습니다.
맨 아래에 id = {Long@9395}5를 확인할 수 있습니다. ByteBuddyInterceptor
라는 객체에서 관리되고 있었습니다.
다른 값인데 우연히 5와 일치하는거 아닌가라는 생각이 들었습니다. 따라서, member의 team_id를 다른 값으로 변경하고 확인해보았는데, 변경한 team_id 값이 ByteBuddyInterceptor의 id 필드에 할당된 것을 확인했습니다.
N:1, N(Member)은 1(Team)을 Lazy Loading 하는 관계에서 N이 가지고 있는 1의 pk는 hibernate의 ByteBuddyInterceptor 객체에서 관리
됩니다.
조금 더 구체적으로 이야기하자면, ByteBuddyInterceptor는 아래와 같은 상속구조를 가집니다.
ByteBuddyInterceptor는 BasicLazyInitializer를 상속하고, BasicLazyInitializer는 AbstractLazyIntializer를 상속합니다. 위에서 본 id는 AbstractLazyIntializer에 존재합니다. 그리고 이를 상속하여 ByteBuddyInterceptor에서 사용하는 것입니다.
이들은 다 hibernate의 객체들입니다.
@Test
void myTest() {
Member member = memberRepository.findById(1L)
.orElseThrow(IllegalArgumentException::new);
Team team = member.getTeam();
}
AbstractLazyIntializer.class
위와 같은 team을 조회하는 테스트를 작성하고, 아래의 AbstractLazyIntializer의 getInternalIdentifier
에 breakpoint를 찍어보면 해당 메서드가 호출됨을 확인할 수 있습니다.
@Test
void myTest() {
Member member = memberRepository.findById(1L)
.orElseThrow(IllegalArgumentException::new);
Long teamId = member.getTeam()
.getId();
}
AbstractLazyIntializer.class
또한, teamId를 직접 조회하면 AbstractLazyIntializer의 id를 반환하는 getInternalIdentifier
, getIdentifier
메서드가 모두 호출되는 것을 확인할 수 있습니다.
getInternalIdentifier와 getIdentifier의 차이는 잘 모르겠지만, 확실한 것은 Lazy Loading을 위한 Proxy를 조회할 때 AbstractLazyIntializer의 id가 반환
된다는 것입니다.
N이 1을 Lazy로 Loading관계에서 N이 가지고 있는 1의 PK는 AbstractLazyIntializer에서 관리가 된다.
실제로 생성되는 객체는 AbstractLazyIntializer를 상속하는 ByteBuddyInterceptor이다.
사실 AbstractLazyIntializer가 최상위 클래스가 아닌, LazyIntializer가 있는데요.
다음에는 Lazy Loading하는 객체의 실제 target 객체가 loading 될 때, 어떤 일들이 일어나는지 확인해보겠습니다.
인텔리제이 디버깅 시스템