배경 및 목표
QueryDSL에서 두 엔티티를 JOIN할 때, A 엔티티의 특정 컬럼은 Long 타입이고 B 엔티티의 동일 컬럼은 Int 타입이었습니다. 타입이 달라 직접 JOIN이 안 되므로 QueryDSL의 intValue()를 사용하여 타입을 맞췄는데, 이로 인해 SQL에 CAST 연산이 추가되어 MySQL이 해당 컬럼의 인덱스를 사용하지 못하고 Full Table Scan이 발생했습니다.
graph LR subgraph problem["❌ 문제 발생 과정"] A["A 엔티티: Long"] -->|"JOIN"| B["B 엔티티: Int"] B -->|"타입 불일치"| C["intValue() 사용"] C -->|"SQL에 CAST 추가"| D["인덱스 무시 → Full Scan"] end
목표
- 타입 캐스팅으로 인한 Full Table Scan을 제거해 인덱스를 정상 사용한다.
- 동일 원인의 재발을 막고 팀에 지식을 공유한다.
해결 방법과 해결 후보군
후보군 비교
| 방식 | 설명 | 한계 |
|---|---|---|
intValue() 유지 | 쿼리 레벨 캐스팅 | CAST로 인덱스 미사용 (Full Scan) |
| 네이티브 쿼리 | SQL 직접 작성 | 타입 세이프 상실, 유지보수성 저하 |
| AttributeConverter (채택) | 엔티티 레벨 타입 통일 | CAST 제거 → 인덱스(ref), 타입 세이프 유지 |
1. EXPLAIN으로 원인 발견
운영 중 특정 쿼리의 성능이 예상보다 느려 EXPLAIN을 확인한 결과, type이 ALL(Full Scan)로 나왔습니다.
-- 생성된 SQL (문제)
SELECT * FROM a_table a
JOIN b_table b ON CAST(a.target_id AS SIGNED) = b.id
-- → CAST로 인해 a.target_id의 인덱스 미사용WHERE/JOIN 절에 인덱스가 걸린 컬럼을 사용하고 있는데도 인덱스를 타지 않는 것이 이상하여, 실제 생성되는 SQL을 확인했습니다. intValue()로 인해 CAST(column AS SIGNED) 구문이 추가되어 있었고, MySQL은 컬럼에 함수나 캐스팅이 적용되면 인덱스를 사용하지 않습니다.
2. AttributeConverter로 엔티티 레벨 타입 통일
쿼리 레벨에서 CAST를 제거하려면, 양쪽 엔티티의 타입을 일치시켜야 합니다. B 엔티티의 Int 컬럼에 JPA AttributeConverter를 적용하여, 엔티티 매핑 시점에 Int ↔ Long 변환을 처리하도록 했습니다.
// AttributeConverter로 DB Int ↔ Entity Long 변환
@Converter(autoApply = true)
class IntToLongConverter : AttributeConverter<Long, Int> {
override fun convertToDatabaseColumn(attribute: Long?): Int? = attribute?.toInt()
override fun convertToEntityAttribute(dbData: Int?): Long? = dbData?.toLong()
}
// B 엔티티에서 사용
@Entity
class BEntity(
@Convert(converter = IntToLongConverter::class)
val targetId: Long // DB에는 Int, 엔티티에서는 Long
)이를 통해 QueryDSL에서 intValue() 없이 직접 JOIN이 가능해지고, SQL에서 CAST가 사라져 인덱스가 정상적으로 사용됩니다.
-- 개선 후 SQL (CAST 없음)
SELECT * FROM a_table a
JOIN b_table b ON a.target_id = b.id
-- → 인덱스 정상 사용 (type: ref)graph LR subgraph before["❌ 기존"] Q1["intValue()"] --> C1["CAST"] --> F1["Full Scan (ALL)"] end subgraph after["✅ 개선"] Q2["AttributeConverter"] --> C2["CAST 없음"] --> F2["인덱스 사용 (ref)"] end
3. 포스트 모텀 문서 팀 공유
이 문제는 ORM을 사용하면 누구든 겪을 수 있는 패턴이므로, 원인 분석과 해결 과정을 포스트 모텀 문서로 정리하여 팀에 공유했습니다. “엔티티 간 타입 불일치 시 QueryDSL이 생성하는 SQL에 CAST가 추가되어 인덱스가 무시될 수 있다”는 점과, “ORM이 생성하는 쿼리를 반드시 EXPLAIN으로 확인해야 한다”는 점을 공유하여 같은 문제가 반복되지 않도록 했습니다.
결과
| 지표 | 기존 | 개선 |
|---|---|---|
| 스캔 타입 | ALL (Full Scan) | ref (인덱스) |
| 추가 성과 | - | 포스트 모텀 문서 팀 공유 |
모니터링
- 해당 쿼리 실행 계획의 스캔 타입(ALL→ref)과 조회 행 수를 관측한다.
- 포스트모템 문서로 팀에 공유하고 유사 패턴을 점검한다.