Spring Boot 3 + Kotlin + MongoDB: Custom Repository에서 Query 패턴 완전 정복
Spring Boot 3 + Kotlin + MongoDB 환경에서 Custom Repository에서 Query 객체를 활용해 MongoDB의 유연한 쿼리 기능을 구현하는 실전 패턴을 초보자도 쉽게 이해할 수 있도록 설명합니다. 다양한 실전 예제, 패턴별 장단점, 코드 작성 팁, 공식 문서 및 참고 자료까지 모두 다룹니다.
Custom Repository와 Query 객체란?
Spring Data MongoDB에서 기본 Repository만으로는 복잡한 쿼리나 동적 쿼리, 집계 등 MongoDB의 강력한 기능을 모두 활용하기 어렵습니다. 이때 Custom Repository
와 Query
객체를 활용하면, 자바/Kotlin 코드로 MongoDB의 다양한 쿼리를 유연하게 작성할 수 있습니다.
Custom Repository
: 기본 Repository에 없는 쿼리/로직을 직접 구현할 때 사용Query 객체
: Criteria, Aggregation 등 MongoDB 쿼리를 객체지향적으로 조립할 수 있는 클래스
Custom Repository 기본 구조
interface BookCustomRepository {
fun findBooksByAuthorAndYear(author: String, year: Int): List<Book>
}
class BookCustomRepositoryImpl(
private val mongoTemplate: MongoTemplate
) : BookCustomRepository {
override fun findBooksByAuthorAndYear(author: String, year: Int): List<Book> {
val query = Query()
.addCriteria(Criteria.where("author").`is`(author))
.addCriteria(Criteria.where("publishedYear").`is`(year))
return mongoTemplate.find(query, Book::class.java)
}
}
Query 객체로 자주 쓰는 패턴들
조건 연산자(Comparison Operators)와 조합 연산자(Logical Operators)
MongoDB의 Query 객체는 다양한 조건 연산자를 지원합니다. 아래는 실무에서 자주 쓰이는 주요 연산자와 예시입니다.
- eq / ne (같음/같지 않음)
val query = Query(Criteria.where("category").`is`("IT")) // eq val query = Query(Criteria.where("category").ne("역사")) // ne
MongoDB 쿼리:
// eq { "category": "IT" } // ne { "category": { "$ne": "역사" } }
- gt / gte / lt / lte (크다/이상/작다/이하)
val query = Query(Criteria.where("publishedYear").gte(2020)) // 2020년 이상 val query = Query(Criteria.where("publishedYear").lte(2023)) // 2023년 이하 val query = Query(Criteria.where("price").gt(10000)) // 10000원 초과 val query = Query(Criteria.where("price").lt(50000)) // 50000원 미만
MongoDB 쿼리:
{ "publishedYear": { "$gte": 2020 } } { "publishedYear": { "$lte": 2023 } } { "price": { "$gt": 10000 } } { "price": { "$lt": 50000 } }
- in / nin (포함/미포함)
val query = Query(Criteria.where("author").`in`(listOf("홍길동", "이몽룡"))) val query = Query(Criteria.where("author").nin(listOf("임꺽정", "성춘향")))
MongoDB 쿼리:
{ "author": { "$in": ["홍길동", "이몽룡"] } } { "author": { "$nin": ["임꺽정", "성춘향"] } }
- exists (필드 존재 여부)
val query = Query(Criteria.where("summary").exists(true)) // summary 필드가 존재하는 도큐먼트 val query = Query(Criteria.where("isbn").exists(false)) // isbn 필드가 없는 도큐먼트
MongoDB 쿼리:
{ "summary": { "$exists": true } } { "isbn": { "$exists": false } }
- regex (정규식/LIKE 검색)
val query = Query(Criteria.where("title").regex(".*Kotlin.*", "i")) // title에 'Kotlin'이 포함(대소문자 무시)
MongoDB 쿼리:
{ "title": { "$regex": ".*Kotlin.*", "$options": "i" } }
- andOperator / orOperator (AND/OR 조합)
// AND 조합 (여러 조건 모두 만족) val query = Query().addCriteria( Criteria().andOperator( Criteria.where("author").`is`("홍길동"), Criteria.where("publishedYear").gte(2020) ) )
MongoDB 쿼리:
{ "$and": [ { "author": "홍길동" }, { "publishedYear": { "$gte": 2020 } } ] }
// OR 조합 (여러 조건 중 하나라도 만족) val query = Query( Criteria().orOperator( Criteria.where("category").`is`("IT"), Criteria.where("category").`is`("소설") ) )
MongoDB 쿼리:
{ "$or": [ { "category": "IT" }, { "category": "소설" } ] }
- 복합 조건 예시
val query = Query().addCriteria( Criteria().andOperator( Criteria.where("author").`is`("홍길동"), Criteria.where("publishedYear").gte(2020), Criteria.where("category").`in`(listOf("IT", "소설")) ) )
MongoDB 쿼리:
{ "$and": [ { "author": "홍길동" }, { "publishedYear": { "$gte": 2020 } }, { "category": { "$in": ["IT", "소설"] } } ] }
각 연산자는 조합해서 사용할 수 있으며, 복잡한 동적 쿼리도 안전하게 구현할 수 있습니다. 조건 연산자는 실무에서 매우 자주 활용되니 꼭 익혀두세요!
1. 단일 필드 조건 검색
val query = Query(Criteria.where("title").`is`("Kotlin Guide"))
mongoTemplate.find(query, Book::class.java)
- 특정 필드가 정확히 일치하는 도큐먼트 검색
2. 여러 조건(AND, OR) 조합
val query = Query()
.addCriteria(Criteria.where("author").`is`("홍길동"))
.addCriteria(Criteria.where("publishedYear").gte(2020))
mongoTemplate.find(query, Book::class.java)
// OR 조건
val query = Query(Criteria().orOperator(
Criteria.where("author").`is`("홍길동"),
Criteria.where("author").`is`("이몽룡")
))
mongoTemplate.find(query, Book::class.java)
3. IN, NOT IN 조건
val query = Query(Criteria.where("category").`in`(listOf("IT", "소설")))
mongoTemplate.find(query, Book::class.java)
val query = Query(Criteria.where("category").nin(listOf("역사", "수필")))
mongoTemplate.find(query, Book::class.java)
4. LIKE(부분 일치) 검색
val query = Query(Criteria.where("title").regex(".*Kotlin.*", "i"))
mongoTemplate.find(query, Book::class.java)
- 정규식을 활용한 부분 일치(대소문자 무시)
5. 정렬, 페이징
정렬은 MongoDB 쿼리에서 매우 자주 사용되는 기능입니다. Spring Data의 Sort
객체를 활용하면 여러 필드에 대해 오름차순/내림차순 정렬, 동적 정렬 등 다양한 정렬 옵션을 쉽게 구현할 수 있습니다.
- 단일 필드 내림차순 정렬
val query = Query().with(Sort.by(Sort.Direction.DESC, "publishedYear")) mongoTemplate.find(query, Book::class.java)
- 여러 필드 복합 정렬
val sort = Sort.by( Sort.Order.desc("publishedYear"), Sort.Order.asc("title") ) val query = Query().with(sort) mongoTemplate.find(query, Book::class.java)
-
위 예제는 출간연도는 내림차순, 제목은 오름차순으로 정렬합니다.
- 동적으로 정렬 필드/방향 지정
fun findBooksSorted(field: String, ascending: Boolean): List<Book> { val direction = if (ascending) Sort.Direction.ASC else Sort.Direction.DESC val sort = Sort.by(direction, field) val query = Query().with(sort) return mongoTemplate.find(query, Book::class.java) }
- 페이징과 정렬을 함께 적용
val pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "title")) val query = Query().with(pageable) mongoTemplate.find(query, Book::class.java)
-
위 예제는 0페이지(첫 페이지), 10개씩, 제목 오름차순으로 페이징+정렬합니다.
- 정렬 방향 상수
Sort.Direction.ASC
: 오름차순Sort.Direction.DESC
: 내림차순
정렬은 실시간 검색, 대시보드, 관리자 리스트 등 다양한 실무에서 매우 중요하게 활용됩니다. 여러 필드 복합 정렬, 동적 정렬 등도 모두 쉽게 구현할 수 있습니다.
6. Projection(필드 일부만 조회)
val query = Query(Criteria.where("author").`is`("홍길동"))
.fields().include("title").include("publishedYear")
mongoTemplate.find(query, Book::class.java)
7. 집계(Aggregation) 파이프라인
val aggregation = Aggregation.newAggregation(
Aggregation.match(Criteria.where("publishedYear").gte(2020)),
Aggregation.group("author").count().`as`("bookCount")
)
val results = mongoTemplate.aggregate(aggregation, "books", AuthorBookCount::class.java)
실전 Custom Repository 예제
interface BookCustomRepository {
fun searchBooks(
title: String?,
author: String?,
minYear: Int?,
maxYear: Int?,
categories: List<String>?
): List<Book>
}
class BookCustomRepositoryImpl(
private val mongoTemplate: MongoTemplate
) : BookCustomRepository {
override fun searchBooks(
title: String?, author: String?, minYear: Int?, maxYear: Int?, categories: List<String>?
): List<Book> {
val criteria = mutableListOf<Criteria>()
if (!title.isNullOrBlank()) criteria += Criteria.where("title").regex(".*$title.*", "i")
if (!author.isNullOrBlank()) criteria += Criteria.where("author").`is`(author)
if (minYear != null) criteria += Criteria.where("publishedYear").gte(minYear)
if (maxYear != null) criteria += Criteria.where("publishedYear").lte(maxYear)
if (!categories.isNullOrEmpty()) criteria += Criteria.where("category").`in`(categories)
val query = Query().addCriteria(if (criteria.size == 1) criteria[0] else Criteria().andOperator(*criteria.toTypedArray()))
return mongoTemplate.find(query, Book::class.java)
}
}
Custom Repository 패턴의 장점과 주의점
복잡한 동적 쿼리
를 코드로 안전하게 작성할 수 있다.- MongoDB의 집계, 정렬, 부분조회 등 강력한 기능을 손쉽게 활용할 수 있다.
- 코드 재사용성과 테스트 용이성이 높다.
- 단, 너무 복잡한 쿼리는 Aggregation, MapReduce 등으로 분리하는 것이 유지보수에 유리하다.
참고할 만한 공식 문서 및 레퍼런스
Spring Data MongoDB의 Custom Repository와 Query 객체를 활용하면, MongoDB의 유연한 쿼리 기능을 Kotlin 코드로 안전하고 강력하게 구현할 수 있습니다. 다양한 패턴을 실전에서 직접 활용해 보세요!