Spring Boot 3 + Kotlin + MongoDB 환경에서 Custom Repository에서 Query 객체를 활용해 MongoDB의 유연한 쿼리 기능을 구현하는 실전 패턴을 초보자도 쉽게 이해할 수 있도록 설명합니다. 다양한 실전 예제, 패턴별 장단점, 코드 작성 팁, 공식 문서 및 참고 자료까지 모두 다룹니다.


Custom Repository와 Query 객체란?

Spring Data MongoDB에서 기본 Repository만으로는 복잡한 쿼리나 동적 쿼리, 집계 등 MongoDB의 강력한 기능을 모두 활용하기 어렵습니다. 이때 Custom RepositoryQuery 객체를 활용하면, 자바/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 코드로 안전하고 강력하게 구현할 수 있습니다. 다양한 패턴을 실전에서 직접 활용해 보세요!