잘못된 설계에서 비롯된 문제이지만 기존 Java-Spring프로젝트를 Kotlin-String으로 이주하면서 @OneToOne 관계에서 지연 로딩기능이 동작하지 않는 문제가 생겼다.
하여 오늘은 관련 내용을 공부했다
이유는 다음과 같다.
JPA는 객체의 참조가 프록시 기반으로 동작한다.
즉 연관 관계가 있는 객체는 참조 할때 기본적으로 Null이 아닌 객체를 반환한다.
1:1관계 에서는 Null이 허용되는 경우 프록시 형태로 Null 객체를 반환할 수 없기 때문이다.
(= Nullable한 엔티티에 대해 프록시 객체 생성을 보장할 수 없다)
그런 이유로 JPA구현체는 1:1 관계에서 지연로딩을 허용하지 않고, 값을 즉시(Eager)읽어드린다.
1:N 관계는 이미 배열의 형태로 참조할 프록시 객체를 싸고 있기 때문에 그 객체가 Null이라도 참조할때는 문제가 되지 않는다.
된다. 이런 제약사항을 염두하지 않고 당연히 지연로딩이 되겠다는 가정하게 코딩하면 성능에 심각한 문제를 겪을 수 있다.
예) 1:1 관계의 부모/자식 테이블이 있고 각각의 테이블에 100건의 데이터가 있다고 가정했을때,
부모 테이블 전체를 조회하면 쿼리가 몇번 나갈까?
개발자는 1건의 쿼리 (select * from tbl_부모)를 바랬겠지만 실제로는 101건의 쿼리가 실행된다.
부모테이블에 있는 레코드와 연결된 레코드를 모두 조회하기 때문이다.
위의 조건일 때 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가 없어서 지연로딩을 할 수 없기 때문에 바로 조회하는 것으로 생각된다.
흔히 테이블을 분리(partitioning)했을 때 장점과 단점은 다음과 같다.
장점
관리적 측면
성능적 측면
단점
테이블 하나가 너무 거대해 졌거나 일부 컬럼에 대한 조회가 많을 때,
분리하여 따로 관리하면 조금의 성능 향상을 얻을 수 있지 않을까? (OS에서 배우는 Locality처럼..)