일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Entity
- entity graph
- centos7
- mirror maker2
- producer
- git
- spring
- Spring Data JPA
- CodePipeline
- topic생성
- PAGING
- mysql
- Spring JPA
- CI
- Kotlin
- transactionaleventlistener
- API
- K8s
- JPA
- offsetdatetime
- Streams
- AWS
- spring kafka
- Kubernetes
- consumer
- QueryDSL
- bean
- ECS
- cd
- kafka
- Today
- Total
Yebali
Spring JPA의 @OneToOne 관계와 지연로딩 본문
JPA는 일반적으로 @OneToOne에 지연 로딩을 지원하지 않는다.
JPA는 객체의 참조가 프록시 기반으로 동작한다.
즉 연관 관계가 있는 객체는 참조할 때 기본적으로 Null이 아닌 객체를 반환한다.
1:1 관계에서는 Null이 허용되는 경우 프록시 형태로 Null 객체를 반환할 수 없기 때문이다.
(= Nullable한 엔티티에 대해 프록시 객체 생성을 보장할 수 없다)
그런 이유로 JPA구현체는 1:1 관계에서 지연 로딩을 허용하지 않고, 값을 즉시(Eager) 읽어드린다.
그럼 1:N은?
1:N 관계는 이미 배열의 형태로 참조할 프록시 객체를 싸고 있기 때문에 그 객체가 Null이라도 참조할 때는 문제가 되지 않는다.
지연 로딩이 되지 않는게 문제가 되나요..?
된다.
이런 제약사항을 염두하지 않고 당연히 지연로딩이 되겠다는 가정하게 코딩하면 성능에 심각한 문제를 겪을 수 있다.
예) 1:1 관계의 부모/자식 테이블이 있고 각각의 테이블에 100건의 데이터가 있다고 가정했을 때, 부모 테이블 전체를 조회하면 쿼리가 몇 번 나갈까?
개발자는 1건의 쿼리 (select * from tbl_부모)를 바랐겠지만 실제로는 101건의 쿼리가 실행된다.
부모 테이블에 있는 레코드와 연결된 레코드를 모두 조회하기 때문이다.
하지만 특정 조건들을 만족하면 지연 로딩 기능을 사용할 수 있다.
- Nullable이 허용되지 않는 1:1 관계여야 한다.
- 양방향이 아닌 단방향 1:1 관계여야 한다.
- @PrimaryKeyJoin은 허용되지 않는다.
: 부모, 자식 엔티티 간 조인 칼럼이 모두 PK이면 안된다.
위의 조건일 때 FK를 가진 엔티티 쪽에서만 지연 로딩이 동작한다.
임의로 1:1 관계의 human - IDCard 엔티티를 만들어 조회하는 예시 코드를 작성했다.
@Entity
class Human(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long,
var name: String,
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idcard")
var idCard: IDCard?
) {
constructor() : this(
0L, "", null
)
companion object {
fun createHuman(name:String, idCard: IDCard): Human {
val human = Human()
human.name = name
human.bindIDCard(idCard)
return human
}
}
fun bindIDCard(idCard: IDCard) {
this.idCard = idCard
idCard.human = this
}
}
@Entity
class IDCard (
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long,
@Column(name = "number")
var idNumber: String,
@OneToOne(mappedBy = "idCard", fetch = FetchType.LAZY)
var human: Human?
)
FK를 보유한 Human을 조회했을 때, 코드와 결과는 아래와 같다.
@Test
fun testLazy() {
var idCard = IDCard(id = 0, idNumber = "123-456", null)
val i1 = idCardRepository.save(idCard).id
val human = Human.createHuman("yeseong", idCard)
val i2 = humanRepository.save(human).id
em.flush()
em.clear()
val findOne = humanRepository.findById(i2).get()
}
조회 쿼리 결과 : 실제로 단 1개의 select 쿼리가 실행된다.
select human0_.id as id1_0_0_, human0_.idcard as idcard3_0_0_, human0_.name as name2_0_0_ from human human0_ where human0_.id=1;
FK를 보유하지 않은 IDCard를 조회 했을때, 코드와 결과는 아래와 같다.
@Test
fun testLazy() {
var idCard = IDCard(id = 0, idNumber = "123-456", null)
val i1 = idCardRepository.save(idCard).id
val human = Human.createHuman("yeseong", idCard)
val i2 = humanRepository.save(human).id
em.flush()
em.clear()
val findOne = idCardRepository.findById(i1).get()
}
조회 쿼리 결과 : idcard를 조회하는 쿼리 1개, 해당 id를 가진 human을 조회하는 쿼리 1개
총 2개의 쿼리가 실행된다.
select idcard0_.id as id1_1_0_, idcard0_.number as number2_1_0_ from idcard idcard0_ where idcard0_.id=4;
select human0_.id as id1_0_0_, human0_.idcard as idcard3_0_0_, human0_.name as name2_0_0_ from human human0_ where human0_.idcard=4;
FK를 가지고 있지 않은 엔티티를 조회했을 때에는 fetchType을 Lazy로 해도 즉시 로딩이 되는 걸 볼 수 있다.
단순히 생각하면 FK가 없어서 지연 로딩을 할 수 없기 때문에 바로 조회하는 것으로 생각된다.
여담. 1:1 관계에서 FK가 Non-nullable일 때 테이블을 분리해야 하는 경우가 있을까?
흔히 테이블을 분리(partitioning)했을 때 장점과 단점은 다음과 같다.
장점
- 관리적 측면
- 전체 데이터를 손실할 가능성이 줄어든다.
- 분할된 테이블 별로 백업/복구가 가능하다.
- 분할된 테이블 단위로 I/O 분산이 가능하며 UPDATE 성능이 향상된다.
- 성능적 측면
- 데이터 전체 검색 (full scan) 시 필요한 부분만 탐색하여 성능이 향상된다.
- 필요한 데이터만 빠르게 조회 할 수 있어 쿼리 자체가 단순하고 가볍다.
단점
- 테이블간 Join 비용이 발생한다.
테이블 하나가 너무 거대해 졌거나 일부 컬럼에 대한 조회가 많을 때,
분리하여 따로 관리하면 조금의 성능 향상을 얻을 수 있지 않을까?
'Spring' 카테고리의 다른 글
Spring JPA의 Cache (0) | 2021.10.31 |
---|---|
Spring JPA에 기본 생성자가 필요한 이유 (0) | 2021.10.31 |
Spring JPA Entity에 기본 생성자가 필요한 이유 (0) | 2021.10.11 |
Spring Data JPA의 페이징과 정렬 (0) | 2021.10.11 |
Spring Data JPA의 도메인 클래스 컨버터 (0) | 2021.10.11 |