[JPA] 일대일 관계와 Fetch 전략

이수찬·2023년 4월 11일
0

JPA에서 @OneToOne관계에서 지연 로딩이 동작하지 않는 문제가 있다.
그렇다면 언제 어느 테이블을 조회할 때, 지연 로딩이 작동하지 않는 것일까?

그전에 꼭 알아야 할 개념인 연관관계 주인에 대해 알아보자.

  1. 연관관계 주인

위와 같이 연관관계가 설정되어 있다고 가정해보자.
테이블 관점에서는 join을 통해 Member 테이블은 Locker 테이블을,
Locker 테이블은 Locker 테이블을 알 수 있다.
즉, 테이터베이스 테이블은 외래키 하나로 양쪽 테이블 조인을 통해 서로 알 수 있다.
그러나 객체는 아니다. 객체는 참조형 필드가 존재해야 다른 객체를 참조하는 것이 가능하다.
객체는 단방향 연관관계가 설정되어 있으면, Member는 어떤 Locker와 연결되어 있는지 Member 테이블 조회를 통해(fk가 존재하므로) 알 수 있지만, Locker의 경우 어떤 Member와 연결되어 있는지 Locker 테이블 조회를 통해(Locker테이블에는 fk가 존재하지 않기에) 알 수 없다.

그래서 객체 관점에서는 양방향 연관관계를 통해 이 문제를 해결한다.
@mappedBy를 통해 Locker는 어떤 Member와 연결되어 있는지 알 수 있다.

[(참고) Member 테이블의 경우 JPA에서는 referencedColumnName을 통해 default로 대상 테이블의 pk값을 지정하여 매핑한다.]

여기서 연관관계 주인이 등장한다.
객체 관점에서는 테이블에 존재하는 외래키를 관리해야 하는데, 이 외래키를 관리하는 주인을 연관관계 주인이라 한다.
(연관관계 주인이 테이블의 외래키와 연관관계 매핑)
연관관계 주인은 양방향 연관관계에서 나오는 개념으로 연관관계 주인만이 외래키를 관리(등록, 수정)한다.
주인이 아닌쪽은 읽기만 가능하며, mappedBy 속성으로 주인을 지정한다.

이제 일대일 연관관계에서 단방향 연관관계일 경우와 양방향 연관관계일 경우의 fetch전략에 대해 알아보자.

  1. 일대일 단방향 연관관계에서 Fetch 전략

user entity

@Entity
@Getter
public class User {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id")
    private Long id;

    private String name;

    @OneToOne(fetch = LAZY)
    @JoinColumn(name = "locker_id")
    private Locker locker;
    
}

locker entity

@Entity
@Getter
public class Locker {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id")
    private Long id;

    private int number;
    
}
   userRepository.findById(savedUser.getId()).orElseThrow(RuntimeException::new);

user의 아이디를 통해 user를 가져온다.

Hibernate: 
    select
        u1_0.id,
        u1_0.locker_id,
        u1_0.name 
    from
        user u1_0 
    where
        u1_0.id=?

위의 쿼리를 보면, user의 아이디를 통해 user를 찾는 쿼리 1개만 날라간다.
주 테이블에서 대상테이블을 조회할 때, fetch 전략을 지연로딩으로 설정하면, 지연로딩이 정상적으로 작동하는 것을 알 수 있다.

  1. 일대일 양방향 연관관계에서 Fetch 전략

user entity

@Entity
@Getter
public class User {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id")
    private Long id;

    private String name;

    @OneToOne(fetch = LAZY)
    @JoinColumn(name = "locker_id")
    private Locker locker;
    
}

locker entity

@Entity
@Getter
public class Locker {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id")
    private Long id;

    private int number;

    @OneToOne(mappedBy = "locker", fetch = LAZY)
    private User user;
    
}

3-1. 주 테이블 조회

User findUser = userRepository.findById(savedUser.getId()).orElseThrow(RuntimeException::new);

쿼리를 살펴보면, 지연로딩이 잘 동작하는 것을 알 수 있다.

Hibernate: 
    select
        u1_0.id,
        u1_0.locker_id,
        u1_0.name 
    from
        user u1_0 
    where
        u1_0.id=?

3-2. 대상 테이블 조회

Locker findLocker = lockerRepository.findById(savedLocker.getId())
            .orElseThrow(RuntimeException::new);

쿼리를 살펴보면, 지연로딩이 동작하지 않고, 즉시로딩이 동작하는 것을 알 수 있다.

Hibernate: 
    select
        l1_0.id,
        l1_0.number 
    from
        locker l1_0 
    where
        l1_0.id=?
Hibernate: 
    select
        u1_0.id,
        u1_0.locker_id,
        u1_0.name 
    from
        user u1_0 
    where
        u1_0.locker_id=?

locker 테이블만 조회했을 뿐인데, user 테이블에 locker_fk를 확인하는 쿼리를 날리는 것을 볼 수 있다.

일대일 양방향 연관관계에서 주 테이블이 아닌, 대상 테이블을 조회하는 순간 fetch 타입을 지연로딩으로 설정해도 즉시 로딩이 되는 것을 알 수 있다.

Member와 Locker의 관계에서 Member가 Locker의 FK를 지니고 있는 상황에서 Member에는 Locker의 정보인 FK를 가지고 있지만, Locker에서는 Member에 관한 값이 존재하지 않는다. 따라서 Locker에서 Member 값을 가져오려면 Member를 조회해야 한다.

주 테이블에 외래키가 존재하는 경우, 주 테이블의 외래키를 확인해 값이 있으면 필드에 값이 있다 해주고, 값이 없으면 주 테이블의 필드에 "값이 null이다" 라 해주면 되는데(즉, 주 테이블만 조회해서 확인하면 되는데),
주 테이블에서 연관관계 매핑한 엔티티가 주 테이블에 연결되어 있는지 확인하려면, 주 테이블만 조회해서는 알 수 없다.
연관관계 매핑한 테이블의 외래키를 확인해서 주테이블의 ID가 존재하는지 확인해야 한다.
=> where 문을 통해 값이 있는지 없는지 알아야 프록시를 만들 수 있다.

JPA의 경우 프록시 객체를 만들기 위해서는 연관 객체에 값이 있는지 없는지 알아야 하는데 Locker에는 Member에 대한 값이 없기 때문에 프록시 객체를 만들지 못한다. 또한 쿼리가 N+1로 나가니 프록시를 굳이 만들 필요도 없다. (프록시 객체를 만들기 위한 리소스만 낭비되기에)

반대로 Member에서 Locker를 조회할 때는 Locker의 외래키가 존재하기 때문에, 프록시 객체를 생성하고 지연 로딩으로 쿼리를 수행한다.

<정리>

  1. 일대일 단방향
  • 주 테이블 조회 : 지연 로딩 작동
  • 대상 테이블 조회 : 지연 로딩 작동
  1. 일대일 양방향
  • 주 테이블 조회 : 지연 로딩 작동
  • 대상 테이블 조회 : 지연 로딩 불가능 => 즉시 로딩 작동

<참고자료>
: 자바 ORM 표준 JPA 프로그래밍 - 기본편
https://www.inflearn.com/course/ORM-JPA-Basic

0개의 댓글