`@OneToOne` 연관관계에서 `FetchType.LAZY`를 설정했지만 실제로는 Eager Loading이 발생하는 경우가 있다.
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
// 연관관계의 주인이 아닌 쪽 - Lazy Loading이 동작하지 않음
@OneToOne(mappedBy = "book", fetch = FetchType.LAZY)
private Manuscript manuscript;
}
@Entity
public class Manuscript {
@Id
@GeneratedValue
private Long id;
private byte[] file;
// 연관관계의 주인 - Lazy Loading이 정상 동작
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fk_book")
private Book book;
}
다음과 같은 코드에서 `Manuscript`(연관관계의 주인)의 Lazy Loading은 동작하지만, `Book`(연관관계의 주인이 아닌 쪽)의 Lazy Loading은 동작하지 않는다.
왜 Lazy Loading이 동작하지 않을까?
Hibernate는 연관된 엔티티가 없으면 null로 초기화하고, 있으면 Lazy Loading이 설정되어 있을 경우 프록시 객체로 초기화한다. 하지만 데이터베이스의 테이블 관점에서 보면, 연관관계의 주인이 아닌 엔티티는 연관관계를 참조할 FK가 없기 때문에 연관관계의 존재 여부를 알 수 없다. 따라서, Hibernate는 참조해야 하는 레코드의 객체를 결정할 수 없기 때문에 연관된 엔티티를 즉시 가져와 Eager Loading이 수행된다.
예를 들어, 위 코드에선 `Book`의 `manuscript`속성을 null 또는 proxy 객체 중 무엇으로 초기화할지 결정하기 위해 `Manuscript` 테이블을 조회하고 해당 Book을 참조하는 레코드가 있는지 확인하고 Eager Loading을 수행한다.
어떻게 해결할까?
1. `@MapsId` 사용하기
`@MapsId`를 사용하면 부모 엔티티(`Book`)의 ID를 자식 엔티티(`Manuscript`)의 ID 값으로 함께 사용할 수 있다. 즉, 두 테이블이 같은 PK를 공유하는 1:1 관계가 된다.
이렇게 되면 `Book`을 조회할 때 `Manuscript`의 존재 여부를 확인하기 위해 `Manuscript` 테이블을 뒤져볼 필요가 없다. `Book`의 ID와 같은 ID를 가진 `Manuscript`가 있는지 바로 확인 가능하며, Hibernate는 이 관계를 확신하고 프록시객체를 만들어낼 수 있다.
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
// ✅ manuscript 참조 완전히 제거
// @OneToOne(mappedBy = "book", fetch = FetchType.LAZY)
// private Manuscript manuscript;
}
@Entity
public class Manuscript {
@Id // @GeneratedValue 제거
private Long id;
// ...
@OneToOne(fetch = FetchType.LAZY)
@MapsId // Book의 ID를 이 엔티티의 ID로 사용
@JoinColumn(name = "id") // 컬럼명도 id로 맞춤
private Book book;
}
- 장점: 데이터베이스 스키마 수준에서 1:1 관계를 보장하며, Lazy Loading이 완벽하게 동작한다.
- 단점: `Manuscript`가 `Book`에 완전히 종속되어 독립적으로 존재할 수 없게 된다. (`Book`이 있어야만 `Manuscript` 생성 가능)
2. `@OneToMany` / `@ManyToOne`으로 대체하기
1:1 관계를 1:N 관계로 표현하는 방법이다. `Book` 입장에서 `Manuscript`가 1개만 존재하므로, `List<Manuscript>`로 조회하더라도 항상 0개 또는 1개의 원소만 가지게 된다.
이 방법은 `@OneToMany`의 기본 Fetch 전략이 `LAZY`이므로, 우리가 원하는 지연 로딩이 자연스럽게 동작한다.
@Entity
public class Book {
// ...
// @OneToOne 대신 @OneToMany 사용
@OneToMany(mappedBy = "book", fetch = FetchType.LAZY)
private List<Manuscript> manuscripts = new ArrayList<>();
}
@Entity
public class Manuscript {
// ...
// @OneToOne 대신 @ManyToOne 사용
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fk_book")
private Book book;
}
- 장점: 간단하고 직관적으로 Lazy Loading 문제를 해결할 수 있다.
- 단점: 모델링 관점에서 1:1 관계가 아닌 것처럼 보일 수 있다.
'Spring > Spring Data JPA' 카테고리의 다른 글
| [Spring Data JPA] @Transactional(readOnly = true)를 사용해야 하는 이유 (3) | 2025.08.14 |
|---|---|
| [Spring Data JPA] ID에 관하여 (2) | 2025.03.13 |
| [Spring Data JPA] JPA의 N + 1 문제 (0) | 2025.03.07 |
| [Spring Data JPA] 엔티티 매니저(Entity Manager) (0) | 2025.03.05 |
| [Spring Data JPA] 영속성 컨텍스트(Persistence Context) (0) | 2025.03.04 |