[Project] Kotlin + Spring Boot + Querydsl을 이용한 주소 검색 API
하게 된 계기
- 회사 서비스에서 외부 주소 API가 자주 삑난다고 주소 검색 내재화 API를 만든다는 말이 있었다.
- 스프링 부트도 못하고 JPA도 잘 모르는 나로썬 좀 걱정이 되어 혼자 토이 프로젝트를 진행해 보았다.
Stack
Server : Spring Boot, Spring Data Jpa, Querydsl
Database : MySQL
과정
1. Querydsl 연동
build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.6.6"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.10"
kotlin("plugin.spring") version "1.6.10"
kotlin("plugin.jpa") version "1.6.10"
kotlin("kapt") version "1.6.10" // kapt 등록
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
// Q파일 생성 경로
sourceSets["main"].withConvention(org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet::class) {
kotlin.srcDir("$buildDir/generated/source/kapt/main")
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
runtimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
runtimeOnly("mysql:mysql-connector-java")
// querydsl
api("com.querydsl:querydsl-jpa:")
kapt(group = "com.querydsl", name = "querydsl-apt", classifier = "jpa")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
application.yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/[dbname]?serverTimezone=UTC&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: [userid]
password: [userpassword]
jpa:
hibernate:
# ddl-auto: create
properties:
hibernate:
dialect: com.example.practice_qdsl.Dialect.CustomDialect # 커스텀한 Dialect 경로
# show_sql: true
format_sql: true
logging:
level:
org:
hibernate:
SQL: debug
type:
descriptor:
sql: trace
querydslConfig
package com.example.practice_qdsl.Configuration
import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import javax.persistence.EntityManager
import javax.persistence.PersistenceContext
@Configuration
class QuerydslConfig(
@PersistenceContext
private val entityManager: EntityManager
) {
@Bean
fun jpaQueryFactory(): JPAQueryFactory {
return JPAQueryFactory(this.entityManager)
}
}
2. JPA Dialect를 이용한 match-against 기능 등록
- querydsl은 일종의 JPQL 빌더다. (결국엔 JPQL로 변환됨)
- 순수 JPQL에는 match-against 기능을 지원하지 않는다. 이를 사용하기 위해 JPA의 Dialect를 알아야 한다.
Dialect?
- JPA는 보통 직접 SQL을 작성하고 실행한다. 그런데 DBMS 종류마다 사용하는 SQL이 다르다. JPA는 해당 DBMS에 맞춰 SQL을 생성해야 하는데 DBMS의 정보를 모르면 문제가 발생할 수 있다.
- 그래서 JPA에 어떤 DBMS를 사용하는 알려주는 방법이 Dialect를 설정하는 방법이 된다. JPA에 Dialect를 설정할 수 있는 추상화 Dialect Class를 제공하고 설정된 방언으로 각 DBMS에 맞는 구현체를 제공한다.
package com.example.practice_qdsl.Dialect
import org.hibernate.dialect.MySQL57Dialect
import org.hibernate.dialect.function.SQLFunctionTemplate
import org.hibernate.type.StandardBasicTypes
class CustomDialect: MySQL57Dialect() {
init{
registerFunction("match", SQLFunctionTemplate(StandardBasicTypes.INTEGER, "match(?1) against (?2 in boolean mode)"))
}
}
-
MySQL57Dialect를 상속 받은 뒤, Fulltext Search를 사용하기 위해 생성자에서 regsiterFunction로 match - against 함수를 등록한다.
-
검색 모드는 NATURAL LANGUAGE MODE와 BOOLEAN MODE 2가지가 있다. 자연어 검색 모드에서는 전체 테이블의 50% 이상의 레코드가 검색된 키워드를 가지고 있다면, 그 키워드는 검색어로서 의미가 없다고 판단하고 검색 결과에서 배제 시킨다. 주소 데이터를 배제 시키면 당연히 안되므로 각 키워드의 포함과 불포함을 판단하는 BOOLEAN MODE로 선택한다.
-
type이 String이 아니라 INTEGER로 받은 이유는 Expressions.stringTemplate
은 Select 절에서만 사용이 가능. (이유는 잘 모르겠다.. 누군가 알면 댓글로 남겨주시면 감사하겠습니다 ㅎ)
3. Entity와 Repository
Entity
package com.example.practice_qdsl.Entity
import javax.persistence.*
@Entity(name = "ADDRESS_INFO")
data class AddressInfo(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
var id: Long = 0,
@Column(name = "address")
val address: String,
)
AddressInfoRepositoryCustom
package com.example.practice_qdsl.Repository
import com.example.practice_qdsl.Entity.AddressInfo
interface AddressInfoRepositoryCustom {
fun getAddressList(keyword : String) :List<AddressInfo>
fun getLists():List<AddressInfo>
}
AddressInfoRepositoryCustomImpl
package com.example.practice_qdsl.Repository.Impl
import com.example.practice_qdsl.Entity.AddressInfo
import com.example.practice_qdsl.Entity.QAddressInfo
import com.example.practice_qdsl.Repository.AddressInfoRepositoryCustom
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.core.types.dsl.NumberTemplate
import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.stereotype.Repository
@Repository
class AddressInfoRepositoryCustomImpl(
val jpaQueryFactory: JPAQueryFactory,
) : AddressInfoRepositoryCustom {
override fun getAddressList(keyword: String): List<AddressInfo> {
val booleanTemplate: NumberTemplate<*> = Expressions.numberTemplate(Integer::class.java,
"function('match',{0},{1})",
QAddressInfo.addressInfo.address, keyword)
return jpaQueryFactory.select(QAddressInfo.addressInfo).from(QAddressInfo.addressInfo).where(booleanTemplate.gt(0)).fetch()
}
override fun getLists(): List<AddressInfo> {
return jpaQueryFactory.selectFrom(QAddressInfo.addressInfo).fetch()
}
}
- match against을 where 절에서 사용하려고 Expression.numberTamplate 자료형인 booleanTemplate를 만들었다.
- return에 있는 쿼리문을 보면 greather than > 0 이라는 조건을 주어 booleanTemplate에 맞게 쿼리를 날리게 했다.
4. Perfomance 비교
- 주소 데이터 개수 : 357294
1) %Like%
- SELECT * FROM mydb.ADDRESS_INFO where address like("%서울특별시 서대문구 세무서길%") : 0.157 sec
2) match-against
- SELECT FROM mydb.ADDRESS_INFO where match(address) against("서울특별시+서대문구+세무서길" in boolean mode) > 0 : 0.029 sec
약 5배 차이가 난다. 단 검색어가 "서울특별시" 같이 짦은 것들은 %Like%가 더 빨랐다.
즉, 검색어가 길어질 수록 Perfomance는 match-against >>>>>>>> %Like%이 된다.
참고 : https://idlecomputer.tistory.com/337
Author And Source
이 문제에 관하여([Project] Kotlin + Spring Boot + Querydsl을 이용한 주소 검색 API), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@daehoon12/Project-Kotlin-Spring-Boot-Querydsl을-이용한-주소-검색-API저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)