Spring

[Spring] Spring Data - MongoDB

kahnco 2025. 4. 18. 14:44
반응형

개요

Spring 프레임워크 생태계에서 MongoDB와 같은 NoSQL 데이터베이스를 사용하는 Java 애플리케이션 개발을 단순화하는 것은 매우 중요합니다. Spring Data MongoDB는 MongoDB 문서 스타일 데이터 저장소를 사용하는 솔루션 개발에 핵심 Spring 개념을 적용하여 이를 가능하게 합니다. 문서를 저장하고 쿼리하기 위한 높은 수준의 추상화인 "템플릿"을 제공하며, 이는 Spring 프레임워크의 JDBC 지원과 유사점을 가집니다.

이 게시글에서는 Spring Data MongoDB 버전 4.4.4에 초점을 맞춰, 개발자가 이 강력한 프레임워크를 효과적으로 활용하는 데 필요한 모든 것을 다룹니다. 프로젝트 설정부터 시작하여 핵심 개념, 문서 매핑, 기본적인 CRUD 작업, 다양한 쿼리 방법, 그리고 인덱싱, GridFS, Aggregation Framework, Reactive 지원과 같은 고급 기능까지 상세하게 살펴볼 것입니다.

 


소개

Spring Data MongoDB 란?

Spring Data MongoDB는 더 큰 Spring Data 프로젝트의 일부로, Spring 기반 애플리케이션에서 MongoDB 데이터베이스와의 상호작용을 단순화하는 데 중점을 둡니다. 이는 Spring의 핵심 원칙인 IoC(Inversion of Control), 일관된 예외 변환 계층 등을 MongoDB 데이터 접근에 적용합니다. 개발자는 익숙한 Spring 개념을 사용하여 MongoDB의 문서 지향 데이터 모델과 상호작용할 수 있습니다.

 

목표 및 이점

Spring Data MongoDB의 주요 목표는 다음과 같습니다:

  • 보일러플레이트 코드 감소: 반복적인 데이터 접근 로직 작성을 최소화합니다.
  • 일관된 프로그래밍 모델 제공: 저장소별 특정 기능을 유지하면서도 친숙하고 일관된 Spring 기반 프로그래밍 모델을 제공합니다.
  • 개발 생산성 향상: MongoDB 관련 개발 작업을 단순화하고 가속화합니다.

 

대상 버전 및 호환성

이 가이드는 Spring Data MongoDB 4.4.4 버전을 기준으로 작성되었습니다. Spring Data MongoDB 4.4.x 버전은 일반적으로 MongoDB 서버 4.4.x 버전 이상 및 호환되는 MongoDB Java 드라이버(예: 4.x 버전대)와 함께 사용됩니다. 특정 버전 호환성은 공식 문서를 참조하는 것이 좋습니다.

 

핵심 추상화: Repository 와 Template

Spring Data MongoDB는 MongoDB와 상호작용하는 두 가지 주요 추상화 방법을 제공합니다:

  • Repositories: MongoRepository와 같은 인터페이스를 정의하면 Spring Data가 런타임에 구현을 자동으로 제공합니다. 이는 CRUD 작업과 규약 기반 쿼리(Derived Queries)에 이상적입니다.
  • Templates: MongoTemplate은 MongoDB 작업을 위한 더 낮은 수준의 추상화를 제공하여 복잡한 쿼리, 업데이트, 집계(aggregation) 작업 등에 대한 더 많은 제어권을 제공합니다.

 


프로젝트 설정하기

Spring Boot 애플리케이션에서 Spring Data MongoDB를 사용하기 위한 초기 설정 과정을 알아봅니다.

 

의존성 설정 (Maven/Gradle)

가장 먼저 필요한 의존성을 프로젝트에 추가해야 합니다. Spring Boot에서는 spring-boot-starter-data-mongodb 스타터를 사용하는 것이 가장 일반적입니다. 이 스타터는 spring-data-mongodb, MongoDB Java 드라이버(동기 및/또는 리액티브), 그리고 필요한 다른 Spring Boot 의존성들을 포함합니다.

Spring Boot 프로젝트의 빌드 도구에 따라 다음 의존성을 추가합니다. 일반적으로 Spring Boot의 부모 POM이나 BOM(Bill of Materials)이 버전을 관리해주므로 명시적인 버전 지정이 필요 없을 수 있지만, 특정 버전(여기서는 4.4.4와 호환되는 Spring Boot 버전)을 사용해야 한다면 해당 버전에 맞는 스타터를 명시해야 합니다. (참고: 아래 예시는 버전 3.4.4를 사용하지만, 실제로는 프로젝트의 Spring Boot 버전에 맞는 spring-boot-starter-data-mongodb 버전을 사용해야 합니다.)

 

Maven (pom.xml):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

 

Gradle (build.gradle - Groovy DSL):

implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
// 버전은 Spring Boot 플러그인 또는 BOM에서 관리되므로 보통 생략 가능

 

Gradle (build.gradle.kts - Kotlin DSL):

implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
// 버전은 Spring Boot 플러그인 또는 BOM에서 관리되므로 보통 생략 가능

 

MongoDB 연결 설정 (application.properties / application.yml)

 

Spring Boot는 application.properties 또는 application.yml 파일에 명시된 설정을 기반으로 MongoDB 연결을 자동으로 구성합니다. 별도의 설정이 없다면 기본적으로 mongodb://localhost:27017/test에 연결을 시도합니다.

연결 설정 방법은 크게 두 가지입니다:

1. 개별 속성 사용:

호스트, 포트, 데이터베이스 이름, 사용자 이름, 비밀번호 등을 개별 속성으로 지정하는 방식입니다. 이는 구성이 명확하고 이해하기 쉽다는 장점이 있습니다.

  • application.properties 예시:
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=mydatabase
spring.data.mongodb.username=myuser
spring.data.mongodb.password=mypassword
# spring.data.mongodb.authentication-database=admin # 인증 DB가 다를 경우 지정
  • application.yml 예시:
spring:
  data:
    mongodb:
      host: localhost
      port: 27017
      database: mydatabase
      username: myuser
      password: mypassword
      # authentication-database: admin # 인증 DB가 다를 경우 지정

 

2. 연결 URI 사용:

 

단일 uri 속성을 사용하여 모든 연결 정보를 한 번에 지정하는 방식입니다. 이는 특히 MongoDB Atlas와 같이 SRV 레코드를 사용하거나 복잡한 연결 옵션이 필요한 경우에 유용하며, 더 간결합니다.

  • application.properties 예시:
# 로컬 연결 예시
# spring.data.mongodb.uri=mongodb://myuser:mypassword@localhost:27017/mydatabase
# Atlas SRV 연결 예시
spring.data.mongodb.uri=mongodb+srv://myuser:mypassword@myatlascluster.mongodb.net/mydatabase?retryWrites=true&w=majority
  • application.yml 예시:
spring:
  data:
    mongodb:
      # 로컬 연결 예시
      # uri: mongodb://myuser:mypassword@localhost:27017/mydatabase
      # Atlas SRV 연결 예시
      uri: mongodb+srv://myuser:mypassword@myatlascluster.mongodb.net/mydatabase?retryWrites=true&w=majority

주의: 개별 속성 (host, port 등)과 uri 속성은 동시에 사용할 수 없습니다. 함께 사용하면 설정이 모호해져 애플리케이션 시작 시 오류가 발생할 수 있습니다. 따라서 개발 환경(예: 로컬 MongoDB)에서는 개별 속성을 사용하여 명확성을 높이고, 클라우드 환경(예: MongoDB Atlas)이나 복제 세트(replica set) 등 고급 설정이 필요할 때는 uri 속성을 사용하는 것이 일반적입니다. 이는 Spring Boot가 단순성과 유연성 사이에서 균형을 맞추려는 설계 철학을 반영합니다. 개발자는 상황에 맞춰 더 적합한 방식을 선택해야 합니다.

 

3. 주요 MongoDB 연결 속성:

다음 표는 application.properties 또는 application.yml에서 자주 사용되는 spring.data.mongodb.* 속성들을 요약한 것입니다.

속성 설명 기본값
spring.data.mongodb.uri MongoDB 연결 URI. 설정 시 host, port, username, password 등 개별 속성은 무시됨. mongodb://localhost/test
spring.data.mongodb.host MongoDB 서버 호스트 이름 또는 IP 주소. (uri 미설정 시 사용) localhost
spring.data.mongodb.port MongoDB 서버 포트 번호. (uri 미설정 시 사용) 27017
spring.data.mongodb.database 연결할 데이터베이스 이름. test (uri에서 파생)
spring.data.mongodb.username 인증 사용자 이름. (없음)
spring.data.mongodb.password 인증 비밀번호. (없음)
spring.data.mongodb.authentication-database 인증에 사용할 데이터베이스 이름. 지정하지 않으면 database 속성값을 사용. (없음)
spring.data.mongodb.replica-set 연결할 복제 세트 이름. (없음)
spring.data.mongodb.ssl.enabled SSL 연결 사용 여부. (Spring Boot 2.x에서는 spring.data.mongodb.ssl.enabled 대신 spring.data.mongodb.ssl 사용) false
spring.data.mongodb.auto-index-creation 애플리케이션 시작 시 엔티티 기반 인덱스 자동 생성 여부. true (주의: 최신 버전에서는 false일 수 있음)
spring.data.mongodb.grid-fs-database GridFS 작업을 위한 데이터베이스 이름. 지정하지 않으면 기본 데이터베이스 사용. (없음)

 

Java 기반 설정 (선택 사항)

대부분의 Spring Boot 애플리케이션에서는 속성 파일을 통한 설정으로 충분합니다. 하지만 더 복잡한 MongoClient 설정이나 커스터마이징이 필요한 경우, Java 기반 설정 클래스(예: AbstractMongoClientConfiguration 상속)를 사용할 수 있습니다.

 


핵심 개념 이해하기

Spring Data MongoDB를 효과적으로 사용하기 위해 알아야 할 핵심 개념들을 살펴봅니다.

 

Document

MongoDB는 데이터를 BSON(Binary JSON) 형식의 문서(Document) 로 저장합니다. 이는 관계형 데이터베이스의 행(row)과 유사하지만, 유연한 스키마를 가지며 중첩된 구조나 배열을 포함할 수 있습니다. Spring Data MongoDB에서는 일반적인 Java 객체(POJO, Plain Old Java Object)를 이러한 MongoDB 문서에 매핑하여 사용합니다.

 

Repository

저장소(Repository) 패턴은 데이터 접근 로직을 추상화하는 방법입니다. Spring Data는 다양한 수준의 저장소 인터페이스를 제공하며, MongoRepository는 MongoDB에 특화된 기능을 제공합니다.  

  • Repository<T, ID>: 가장 기본적인 마커 인터페이스로, Spring Data가 저장소 인터페이스를 식별하는 데 사용됩니다. 관리할 도메인 클래스(T)와 식별자 타입(ID)을 제네릭으로 받습니다.  
     
  • CrudRepository<T, ID>: Repository를 상속하며 기본적인 CRUD(Create, Read, Update, Delete) 기능을 제공합니다. save(), findById(), findAll(), count(), delete(), existsById() 등의 메서드를 포함합니다.  
     
  • PagingAndSortingRepository<T, ID>: CrudRepository를 상속하며 페이징(findAll(Pageable)) 및 정렬(findAll(Sort)) 기능을 추가로 제공합니다.  
     
  • MongoRepository<T, ID>: PagingAndSortingRepository를 상속하며, CRUD, 페이징, 정렬 기능 외에도 MongoDB 관련 특정 기능(예: Example 쿼리, 특정 쿼리 메서드)을 제공합니다. 대부분의 MongoDB 애플리케이션에서 주로 사용되는 인터페이스입니다.  
     
  • ReactiveMongoRepository<T, ID>: 리액티브 프로그래밍 모델을 지원하는 저장소 인터페이스입니다. 메서드들은 Mono 또는 Flux와 같은 리액티브 타입을 반환하여 논블로킹(non-blocking) 데이터 접근을 가능하게 합니다.  
     

이러한 계층적 구조는 Spring Data 프로젝트 전반에 걸쳐 일관성을 제공합니다. 개발자는 CrudRepository의 기본 메서드를 다른 Spring Data 프로젝트(예: Spring Data JPA)와 유사하게 사용할 수 있으며, 필요에 따라 MongoRepository가 제공하는 MongoDB 고유의 기능으로 확장할 수 있습니다. 이는 추상화와 특정 저장소의 강력한 기능 활용 사이의 균형을 제공합니다.

 

Template

템플릿(Template)은 저장소보다 더 낮은 수준의 추상화를 제공하며, MongoDB 데이터베이스와 직접 상호작용할 수 있는 다양한 메서드를 제공합니다.  

  • MongoTemplate: 동기(synchronous) 방식의 MongoDB 작업을 위한 핵심 클래스입니다. Spring의 JdbcTemplate과 유사한 디자인을 가지며 , 문서 저장, 업데이트, 삭제, 쿼리 실행, 집계(aggregation), 인덱스 관리 등 광범위한 기능을 제공합니다. 저장소 인터페이스만으로는 구현하기 어려운 복잡한 작업이나 세밀한 제어가 필요할 때 유용합니다.  
     
  • ReactiveMongoTemplate: 리액티브 스택을 위한 MongoTemplate의 비동기, 논블로킹 버전입니다. Mono와 Flux를 반환하여 리액티브 애플리케이션에서 MongoDB 작업을 수행하는 데 사용됩니다.  
     

저장소는 규약을 통한 단순성과 생산성에 초점을 맞추는 반면, 템플릿은 MongoDB의 기능을 최대한 활용하고 복잡한 작업을 수행할 수 있는 유연성과 제어력을 제공합니다. 많은 경우 저장소 인터페이스로 충분하지만, 특정 요구사항이나 성능 최적화를 위해서는 템플릿 사용이 필요할 수 있습니다. 이 두 가지 접근 방식은 상호 보완적이며, 필요에 따라 함께 사용할 수도 있습니다.

 

핵심 Spring Data MongoDB 인터페이스 요약:

인터페이스 상속 주요 목적 및 기능 사용 시점
Repository - 마커 인터페이스, 저장소 식별 직접 사용 거의 없음
CrudRepository Repository 기본적인 CRUD 작업 (save, findById, findAll, delete 등) 제공 간단한 CRUD 기능만 필요할 때
PagingAndSortingRepository CrudRepository CRUD + 페이징 (findAll(Pageable)) 및 정렬 (findAll(Sort)) 기능 제공 페이징 또는 정렬 기능이 필요할 때
MongoRepository PagingAndSortingRepository CRUD, 페이징, 정렬 + MongoDB 특화 기능 (Derived Queries, Example Queries 등) 제공 대부분의 Spring Data MongoDB 애플리케이션에서 권장되는 기본 저장소 인터페이스
ReactiveMongoRepository ReactiveCrudRepository, ReactiveSortingRepository 리액티브 CRUD, 페이징, 정렬 기능 제공 (Mono, Flux 반환) 논블로킹, 리액티브 애플리케이션 구축 시
MongoTemplate - MongoDB 작업에 대한 저수준 동기 API 제공 (쿼리, 업데이트, 집계, 인덱싱 등) 복잡한 쿼리, 대량 업데이트, 집계, 세밀한 제어가 필요할 때
ReactiveMongoTemplate - MongoDB 작업에 대한 저수준 리액티브 API 제공 (Mono, Flux 반환) 리액티브 애플리케이션에서 저수준 제어가 필요할 때
 

Document 와 Repository 정의하기

Java 객체를 MongoDB 문서로 매핑하고, 이를 다루기 위한 Repository 인터페이스를 정의하는 방법을 알아봅니다.

 

POJO 매핑 어노테이션

Spring Data MongoDB는 여러 어노테이션을 사용하여 POJO 클래스와 MongoDB 문서 간의 매핑을 정의합니다.

  • @Document: 클래스 레벨에서 사용되며, 해당 클래스가 MongoDB 문서임을 나타냅니다. collection 속성을 사용하여 매핑될 컬렉션 이름을 명시적으로 지정할 수 있습니다. 지정하지 않으면 클래스 이름을 소문자로 변환한 이름이 기본값으로 사용됩니다.
@Document(collection = "products")
public class Product {
    //...
}

 

  • @Id: 필드 레벨에서 사용되며, 해당 필드가 문서의 고유 식별자(_id)임을 나타냅니다. 필드 이름이 id 또는 _id인 경우 자동으로 식별자로 간주되지만, 다른 이름의 필드를 식별자로 사용하려면 @Id 어노테이션을 명시해야 합니다.
    • ObjectId 매핑: 필드 타입이 org.bson.types.ObjectId인 경우, MongoDB의 ObjectId 타입과 자동으로 매핑됩니다. 저장 시 필드 값이 null이면 MongoDB 드라이버 또는 Spring Data가 자동으로 ObjectId를 생성하여 할당할 수 있습니다.
    • String 매핑: 필드 타입이 String인 경우, 문자열 값이 _id로 저장됩니다. 이 경우 애플리케이션에서 고유한 ID 값을 직접 생성하고 할당해야 합니다.
@Id
private ObjectId internalId; // ObjectId 사용 예

// 또는

@Id
private String productCode; // String 사용 예

 

 
  • @Field: 필드 레벨에서 사용되며, Java 필드 이름과 MongoDB 문서 내의 필드 이름이 다를 경우 매핑할 이름을 지정합니다. value 속성 (또는 속성 이름 생략)을 사용하여 MongoDB 필드 이름을 지정합니다.
@Field("product_name")
private String productName;

@Field("unit_price")
private Double price;

 

주요 매핑 어노테이션 요약:

어노테이션 레벨 설명 주요 속성
@Document 클래스 클래스를 MongoDB 문서로 매핑하고 컬렉션 이름을 지정합니다. collection
@Id 필드 필드를 문서의 기본 키(_id)로 지정합니다. -
@Field 필드 Java 필드를 MongoDB 문서의 특정 필드 이름으로 매핑합니다. value (or name)
@Indexed 필드 해당 필드에 대한 단일 필드 인덱스를 생성하도록 지정합니다. unique, direction, sparse, expireAfterSeconds
@CompoundIndex 클래스 여러 필드를 포함하는 복합 인덱스를 생성하도록 클래스 레벨에서 지정합니다. def, name, unique, sparse
@Transient 필드 해당 필드를 영속성 대상에서 제외합니다 (DB에 저장되지 않음). -
 

타입 매핑

 

  • 기본 타입: String, Integer, Long, Double, Boolean 등 표준 Java 타입은 해당하는 BSON 타입으로 자동 매핑됩니다.
  • ObjectId: @Id 어노테이션이 붙은 org.bson.types.ObjectId 타입 필드는 MongoDB의 ObjectId 타입으로 매핑되며, 자동 생성을 지원합니다.
     
  • 날짜 및 시간 타입: java.util.Date 및 Java 8의 java.time 패키지 타입들(LocalDate, LocalDateTime, Instant 등)은 기본적으로 MongoDB의 Date 타입으로 매핑됩니다. Spring Data MongoDB는 이러한 타입들에 대한 내장 컨버터를 제공합니다.

 

예제: Domain Entity 정의

다음은 @Document, @Id, @Field를 사용한 간단한 Product 엔티티 예제입니다.

package com.example.myapp.domain;

import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

import java.time.LocalDateTime;

@Document(collection = "products") // "products" 컬렉션에 매핑
public class Product {

    @Id // 이 필드를 _id로 사용
    private ObjectId id;

    @Field("product_name") // MongoDB 필드 이름을 "product_name"으로 지정
    private String name;

    private String category; // 필드 이름 그대로 "category"로 매핑

    private double price; // 필드 이름 그대로 "price"로 매핑

    @Field("created_at")
    private LocalDateTime createdAt; // MongoDB Date 타입으로 매핑

    // 생성자, Getter, Setter 등 생략

    public Product(String name, String category, double price) {
        this.name = name;
        this.category = category;
        this.price = price;
        this.createdAt = LocalDateTime.now();
    }

    // Getters and Setters...
    public ObjectId getId() { return id; }
    public void setId(ObjectId id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getCategory() { return category; }
    public void setCategory(String category) { this.category = category; }
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }

    @Override
    public String toString() {
        return "Product{" +
               "id=" + id +
               ", name='" + name + '\'' +
               ", category='" + category + '\'' +
               ", price=" + price +
               ", createdAt=" + createdAt +
               '}';
    }
}

 

 

예제: Repository 인터페이스 정의

위 Product 엔티티를 위한 Repository 인터페이스는 다음과 같이 정의할 수 있습니다.

 

동기 방식:

package com.example.myapp.repository;

import com.example.myapp.domain.Product;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ProductRepository extends MongoRepository<Product, ObjectId> {

    // 예시: 카테고리로 상품 목록 찾기 (Derived Query)
    List<Product> findByCategory(String category);

    // 예시: 특정 가격보다 비싼 상품 목록 찾기 (Derived Query)
    List<Product> findByPriceGreaterThan(double price);
}

 

리액티브 방식:

package com.example.myapp.repository;

import com.example.myapp.domain.Product;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;

@Repository
public interface ReactiveProductRepository extends ReactiveMongoRepository<Product, ObjectId> {

    // 예시: 카테고리로 상품 목록 찾기 (Reactive Derived Query)
    Flux<Product> findByCategory(String category);

    // 예시: 특정 가격보다 비싼 상품 목록 찾기 (Reactive Derived Query)
    Flux<Product> findByPriceGreaterThan(double price);
}

 


CRUD 작업 수행하기

MongoRepository (또는 ReactiveMongoRepository) 인터페이스를 사용하여 MongoDB 문서에 대한 기본적인 생성(Create), 읽기(Read), 수정(Update), 삭제(Delete) 작업을 수행하는 방법을 알아봅니다. 이 인터페이스들은 CrudRepository를 상속하므로 표준 CRUD 메서드를 바로 사용할 수 있습니다.

 

Repository 사용

Spring의 의존성 주입(Dependency Injection)을 사용하여 서비스나 컴포넌트에서 Repository 인스턴스를 주입받아 사용합니다.

@Service
public class ProductService {

    private final ProductRepository productRepository;
    // 또는 private final ReactiveProductRepository productRepository;

    @Autowired
    public ProductService(ProductRepository productRepository) { // 또는 ReactiveProductRepository
        this.productRepository = productRepository;
    }

    // CRUD 메서드 구현...
}

 

코드 예제 (동기 방식 - MongoRepository)

다음은 ProductRepository를 사용한 CRUD 작업 예제입니다.

 

Create (생성) / Update (수정):

 

save() 메서드는 주어진 엔티티를 저장합니다. 만약 엔티티에 ID(@Id 필드) 값이 없거나 해당 ID를 가진 문서가 컬렉션에 없다면 새로운 문서를 삽입(Insert)합니다. 만약 ID 값이 있고 해당 ID의 문서가 이미 존재한다면 기존 문서를 덮어쓰는 방식으로 업데이트(Update)합니다.

// 새로운 Product 생성 및 저장
Product newProduct = new Product("Laptop", "Electronics", 1200.00);
Product savedProduct = productRepository.save(newProduct);
System.out.println("Saved Product: " + savedProduct);

// 기존 Product 조회 및 수정 후 저장 (Update)
ObjectId existingProductId = savedProduct.getId();
Optional<Product> optionalProduct = productRepository.findById(existingProductId);
if (optionalProduct.isPresent()) {
    Product productToUpdate = optionalProduct.get();
    productToUpdate.setPrice(1150.00);
    Product updatedProduct = productRepository.save(productToUpdate); // ID가 있으므로 Update 수행
    System.out.println("Updated Product: " + updatedProduct);
}

 

Read (조회):

  • ID로 조회: findById(id) 메서드는 주어진 ID에 해당하는 문서를 Optional<Entity> 형태로 반환합니다.
ObjectId productId = /* 조회할 상품 ID */;
Optional<Product> productOptional = productRepository.findById(productId);
if (productOptional.isPresent()) {
    System.out.println("Found Product by ID: " + productOptional.get());
} else {
    System.out.println("Product not found with ID: " + productId);
}
  • 전체 조회: findAll() 메서드는 컬렉션의 모든 문서를 List<Entity> (또는 Iterable<Entity>) 형태로 반환합니다.
List<Product> allProducts = productRepository.findAll();
System.out.println("All Products:");
allProducts.forEach(System.out::println);

 

Delete (삭제):

deleteById(id) 또는 delete(entity) 메서드를 사용하여 문서를 삭제할 수 있습니다. deleteAll()은 컬렉션의 모든 문서를 삭제합니다.

ObjectId productIdToDelete = /* 삭제할 상품 ID */;
if (productRepository.existsById(productIdToDelete)) {
    productRepository.deleteById(productIdToDelete);
    System.out.println("Deleted Product with ID: " + productIdToDelete);
}

// 또는 엔티티 객체로 삭제
// Optional<Product> productToDeleteOptional = productRepository.findById(productIdToDelete);
// productToDeleteOptional.ifPresent(productRepository::delete);

// 전체 삭제 (주의해서 사용)
// productRepository.deleteAll();
// System.out.println("All products deleted.");

 

코드 예제 (리액비트 방식 - ReactiveMongoRepository)

리액티브 방식에서는 메서드들이 Mono (0 또는 1개의 결과) 또는 Flux (0개 이상의 결과)를 반환합니다.

 

Create / Update

Product newProduct = new Product("Keyboard", "Accessories", 75.00);
Mono<Product> savedProductMono = reactiveProductRepository.save(newProduct);
savedProductMono.subscribe(saved -> System.out.println("Reactively Saved Product: " + saved));

// Update (fetch, modify, save)
ObjectId existingId = /*... */;
reactiveProductRepository.findById(existingId)
   .flatMap(product -> {
        product.setPrice(70.00);
        return reactiveProductRepository.save(product);
    })
   .subscribe(updated -> System.out.println("Reactively Updated Product: " + updated));

 

Read:

// ID로 조회
ObjectId productId = /*... */;
Mono<Product> productMono = reactiveProductRepository.findById(productId);
productMono.subscribe(
    product -> System.out.println("Reactively Found Product by ID: " + product),
    error -> System.err.println("Error finding product: " + error),
    () -> System.out.println("Product lookup complete.") // Optional: onComplete
);

// 전체 조회
Flux<Product> allProductsFlux = reactiveProductRepository.findAll();
System.out.println("Reactively Found All Products:");
allProductsFlux.subscribe(System.out::println);

 

Delete:

ObjectId productIdToDelete = /*... */;
Mono<Void> deleteMono = reactiveProductRepository.deleteById(productIdToDelete);
deleteMono.subscribe(
    null, // onNext는 없음 (Void)
    error -> System.err.println("Error deleting product: " + error),
    () -> System.out.println("Reactively Deleted Product with ID: " + productIdToDelete) // onComplete
);

// 전체 삭제
// Mono<Void> deleteAllMono = reactiveProductRepository.deleteAll();
// deleteAllMono.subscribe(null, null, () -> System.out.println("All products reactively deleted."));

 

이 예제들은 Spring Data MongoDB Repository를 사용하여 기본적인 데이터 관리 작업을 얼마나 간편하게 수행할 수 있는지 보여줍니다. 다음 섹션에서는 더 복잡한 데이터 조회 방법을 살펴보겠습니다.

 


MongoDB 쿼리하기

Spring Data MongoDB는 데이터를 조회하는 다양한 방법을 제공합니다. 간단한 쿼리부터 복잡한 조건이나 집계가 필요한 쿼리까지, 상황에 맞는 방식을 선택할 수 있습니다.

 

Derived Queries (메서드 이름 기반 쿼리)

가장 간단한 방법은 Repository 인터페이스에 특정 명명 규칙을 따르는 메서드를 선언하는 것입니다. Spring Data MongoDB는 메서드 이름을 분석하여 자동으로 MongoDB 쿼리를 생성합니다.

 

  • 기본 구조: find...By..., read...By..., query...By..., count...By..., get...By... 등의 접두사로 시작하고, By 뒤에 엔티티의 필드 이름을 조합하여 조건을 명시합니다.
  • 조건 결합: And, Or 키워드를 사용하여 여러 필드 조건을 결합할 수 있습니다.
List<Product> findByNameAndCategory(String name, String category);
List<Product> findByNameOrPriceLessThan(String name, double price);

 

  • 비교 연산자: GreaterThan, LessThan, Between, Like, Containing, StartingWith, EndingWith, Exists, True, False, In, NotIn 등 다양한 키워드를 지원합니다.
List<Product> findByPriceGreaterThan(double price);
List<Product> findByNameContainingIgnoreCase(String keyword); // 대소문자 무시 포함 검색
List<Product> findByCategoryIn(List<String> categories);
long countByCategory(String category); // 개수 세기
boolean existsByName(String name); // 존재 여부 확인
  • 정렬: OrderBy 키워드와 필드 이름, 그리고 Asc 또는 Desc를 사용하여 결과를 정렬할 수 있습니다.
List<Product> findByCategoryOrderByPriceDesc(String category); // 카테고리로 찾고 가격 내림차순 정렬
  • 페이징: 메서드 파라미터에 Pageable 인터페이스를 추가하면 페이징 처리가 가능합니다. Pageable 객체에는 페이지 번호, 페이지 크기, 정렬 정보가 포함될 수 있습니다.
Page<Product> findByCategory(String category, Pageable pageable);

// 사용 예시
Pageable pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "name")); // 첫 페이지, 10개씩, 이름 오름차순
Page<Product> productPage = productRepository.findByCategory("Electronics", pageRequest);

 

Derived Queries는 간단하고 직관적이지만, 쿼리가 복잡해지면 메서드 이름이 너무 길어지고 가독성이 떨어질 수 있습니다.

 

@Query 어노테이션 (JSON 기반 쿼리)

메서드 이름만으로 표현하기 어려운 복잡한 쿼리나 MongoDB 고유의 연산자를 사용해야 할 경우, @Query 어노테이션을 사용하여 직접 쿼리 문자열을 정의할 수 있습니다.

 

  • 쿼리 정의: value 속성에 MongoDB의 JSON 기반 쿼리 문자열을 작성합니다.
  • 파라미터 바인딩: 메서드 파라미터를 쿼리 내에서 사용하려면 위치 기반 플레이스홀더 (?0, ?1 등)를 사용합니다. ?0은 첫 번째 파라미터, ?1은 두 번째 파라미터를 의미합니다.
@Query("{ 'name' :?0 }")
List<Product> findProductsByName(String name);

@Query("{ 'category':?0, 'price': { $gte:?1, $lte:?2 } }")
List<Product> findByCategoryAndPriceRange(String category, double minPrice, double maxPrice);

 

  • 필드 선택 (Projection): fields 속성을 사용하여 반환될 문서에서 특정 필드만 선택하거나 제외할 수 있습니다. { 'fieldName': 1 }은 포함, { 'fieldName': 0 }은 제외를 의미합니다. _id는 기본적으로 포함되므로 제외하려면 명시적으로 { '_id': 0 }을 추가해야 합니다.
@Query(value = "{ 'category' :?0 }", fields = "{ 'name' : 1, 'price' : 1, '_id' : 0 }")
List<Product> findNameAndPriceByCategory(String category);
  • 정렬: sort 속성을 사용하여 정렬 순서를 JSON 형식으로 지정할 수 있습니다. { 'fieldName': 1 }은 오름차순, { 'fieldName': -1 }은 내림차순입니다.
@Query(value = "{ 'category' :?0 }", sort = "{ 'price' : -1 }")
List<Product> findByCategoryOrderByPriceDesc(String category);
  • 업데이트 쿼리: @Query는 조회뿐만 아니라 @Update 어노테이션과 함께 사용하여 업데이트 작업을 정의할 수도 있습니다.
@Query("{ '_id' :?0 }")
@Update("{ '$set' : { 'price' :?1 } }")
UpdateResult updateProductPrice(ObjectId id, double newPrice); // UpdateResult 또는 long 반환 가능

 

@Query는 Derived Query보다 유연하며 MongoDB의 다양한 연산자를 활용할 수 있게 해주지만, 쿼리 문자열을 직접 작성해야 하는 부담이 있습니다.

 

Programmatic Queries (Template & Criteria API)

MongoTemplate 또는 ReactiveMongoTemplate을 사용하면 Java 코드를 통해 동적으로 쿼리를 구성하고 실행할 수 있습니다. 이는 조건이 런타임 시점에 결정되거나 매우 복잡한 쿼리를 만들어야 할 때 유용합니다. Criteria API는 쿼리 조건을 객체 지향적인 방식으로 안전하게 작성하도록 돕습니다.  

  • Query 객체 생성: org.springframework.data.mongodb.core.query.Query 객체를 생성합니다.
  • Criteria 객체로 조건 추가: Criteria.where("fieldName")를 시작으로 .is(value), .lt(value), .gt(value), .regex(pattern), .in(collection), .and("otherField"), .orOperator(...) 등 다양한 메서드를 체이닝하여 쿼리 조건을 만듭니다.  
     
  • 쿼리 실행: MongoTemplate의 find(), findOne(), count(), exists() 등의 메서드에 Query 객체와 엔티티 클래스를 전달하여 쿼리를 실행합니다.  
     
  • 리액티브 실행: ReactiveMongoTemplate의 해당 메서드들은 Flux 또는 Mono를 반환합니다.  
// MongoTemplate 사용 예시
@Autowired
private MongoTemplate mongoTemplate;

public List<Product> findProductsDynamically(String name, Double maxPrice, String category) {
    Query query = new Query();
    List<Criteria> criteriaList = new ArrayList<>();

    if (name!= null &&!name.isEmpty()) {
        // 이름에 대한 정규식 검색 (대소문자 무시)
        criteriaList.add(Criteria.where("product_name").regex(name, "i"));
    }
    if (maxPrice!= null) {
        criteriaList.add(Criteria.where("price").lte(maxPrice));
    }
    if (category!= null &&!category.isEmpty()) {
        criteriaList.add(Criteria.where("category").is(category));
    }

    if (!criteriaList.isEmpty()) {
        query.addCriteria(new Criteria().andOperator(criteriaList.toArray(new Criteria)));
    }

    // 예시: 가격 오름차순 정렬 추가
    query.with(Sort.by(Sort.Direction.ASC, "price"));

    return mongoTemplate.find(query, Product.class);
}
 

Template과 Criteria API는 가장 높은 수준의 유연성을 제공하지만, 다른 방법에 비해 코드가 더 장황해질 수 있습니다. Spring Data는 단순성을 위한 규약 기반 방식(findBy...), 선언적 사용자 정의를 위한 어노테이션(@Query), 그리고 완전한 프로그래밍 제어를 위한 API(MongoTemplate/Criteria)를 모두 제공하여 개발자가 상황에 맞는 최적의 도구를 선택할 수 있도록 지원합니다.

 

쿼리 방법 비교:

특징 Derived Queries (메서드 이름) @Query 어노테이션 Template + Criteria API
구현 방식 메서드 시그니처 선언 메서드에 쿼리 문자열 정의 Java 코드로 쿼리 객체 생성
단순성 매우 높음 중간 낮음
유연성 낮음 중간 매우 높음
쿼리 복잡도 간단한 쿼리 중간 ~ 복잡한 정적 쿼리 매우 복잡하거나 동적인 쿼리
타입 안전성 컴파일 시점 (메서드 시그니처) 낮음 (문자열 기반) 높음 (Criteria API 사용 시)
주 사용 사례 간단한 조회, 프로토타이핑 복잡한 정적 쿼리, 특정 연산자 동적 쿼리, 복잡한 조건 조합
 

고급 기능 살펴보기

Spring Data MongoDB는 기본적인 CRUD 및 쿼리 기능 외에도 다양한 고급 기능을 제공하여 애플리케이션 개발을 더욱 효율적으로 만듭니다.

 

인덱싱 (@Indexed, @CompoundIndex)

MongoDB에서 쿼리 성능을 최적화하려면 인덱스를 적절하게 사용하는 것이 필수적입니다. Spring Data MongoDB는 어노테이션을 통해 엔티티 클래스에 인덱스를 선언적으로 정의할 수 있는 편리한 방법을 제공합니다.  

  • @Indexed: 단일 필드 인덱스를 정의합니다. 필드 레벨에 적용합니다.
    • unique = true: 고유 인덱스를 생성합니다.
    • direction = IndexDirection.DESCENDING: 내림차순 인덱스를 생성합니다 (기본값은 ASCENDING).
    • sparse = true: 해당 필드가 없는 문서는 인덱스에서 제외합니다.
    • expireAfterSeconds = 3600: TTL(Time-To-Live) 인덱스를 생성하여 3600초(1시간) 후에 문서가 자동으로 삭제되도록 합니다. (주의: TTL 인덱스는 Date 타입 필드나 Date 배열 필드에만 적용 가능)
@Document(collection = "users")
public class User {
    @Id private ObjectId id;
    @Indexed(unique = true) // email 필드에 고유 인덱스 생성
    private String email;
    @Indexed(direction = IndexDirection.DESCENDING) // age 필드에 내림차순 인덱스 생성
    private int age;
    @Indexed(expireAfterSeconds = 86400) // createdAt 필드에 TTL 인덱스 (1일 후 만료)
    private Date createdAt;
    //...
}

 

  • @CompoundIndex: 여러 필드를 결합한 복합 인덱스를 정의합니다. 클래스 레벨에 적용하며, 여러 개를 정의할 수 있습니다 (@CompoundIndexes 사용).
    • def = "{'lastName': 1, 'firstName': 1}": 인덱스 정의를 JSON 형식으로 지정합니다. 1은 오름차순, -1은 내림차순입니다.
    • name = "user_name_idx": 인덱스 이름을 지정합니다.
    • unique = true, sparse = true 등 @Indexed와 유사한 옵션을 지원합니다.
@Document(collection = "users")
@CompoundIndex(name = "user_name_idx", def = "{'lastName': 1, 'firstName': 1}")
public class User {
    @Id private ObjectId id;
    private String firstName;
    private String lastName;
    //...
}

 

인덱스 자동 생성: Spring Boot는 기본적으로 spring.data.mongodb.auto-index-creation=true 설정을 통해 애플리케이션 시작 시 어노테이션 기반 인덱스를 자동으로 생성하려고 시도합니다. 하지만 최신 버전에서는 이 기본값이 false일 수 있으며 , 특히 운영 환경에서는 인덱스 생성 및 변경 작업이 리소스를 많이 소모하고 예기치 않은 성능 저하를 유발할 수 있으므로, 자동 생성에만 의존하기보다는 명시적인 인덱스 관리 전략(예: 배포 스크립트, Mongock 같은 마이그레이션 도구 사용)을 고려하는 것이 좋습니다. 인덱스 전략은 애플리케이션 성능에 직접적인 영향을 미치므로 신중하게 계획하고 관리해야 합니다.

 

GridFS (대용량 파일 저장)

MongoDB의 BSON 문서 크기 제한(16MB)을 초과하는 대용량 파일(이미지, 비디오, 문서 등)을 저장하고 관리하기 위해 GridFS 명세를 사용합니다. Spring Data MongoDB는 GridFsTemplate과 ReactiveGridFsTemplate을 통해 GridFS 기능을 쉽게 사용할 수 있도록 지원합니다.

  • GridFsTemplate (동기):
    • 파일 저장: store(InputStream content, String filename, String contentType, Object metadata) 메서드 등을 사용하여 파일을 GridFS에 저장합니다. 파일은 여러 개의 청크(chunk)로 분할되어 저장됩니다.
    • 파일 조회: findOne(Query query) 또는 find(Query query)를 사용하여 파일 메타데이터(GridFSFile)를 조회합니다.
    • 파일 내용 접근: 조회된 GridFSFile로부터 GridFsResource를 얻고, getInputStream()을 통해 파일 내용을 스트림으로 읽을 수 있습니다.
    • 파일 삭제: delete(Query query)를 사용하여 파일을 삭제합니다.
@Autowired
private GridFsTemplate gridFsTemplate;

public ObjectId storeFile(InputStream inputStream, String filename, String contentType) {
    return gridFsTemplate.store(inputStream, filename, contentType);
}

public GridFsResource getFile(ObjectId id) {
    GridFSFile file = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id)));
    if (file!= null) {
        return gridFsTemplate.getResource(file);
    }
    return null;
}

public void deleteFile(ObjectId id) {
    gridFsTemplate.delete(Query.query(Criteria.where("_id").is(id)));
}
  • ReactiveGridFsTemplate (리액티브):
    • 동기 버전과 유사한 기능을 제공하지만, Mono와 Flux를 반환하여 논블로킹 방식으로 작동합니다.  
       
    • 파일 저장: store(Flux<DataBuffer> content, String filename,...)은 Mono<ObjectId>를 반환합니다.
    • 파일 조회: findOne(Query query)은 Mono<GridFSFile>, find(Query query)는 Flux<GridFSFile>을 반환합니다.
    • 파일 내용 접근: getResource(GridFSFile file)은 Mono<ReactiveGridFsResource>를 반환하고, 리소스에서 getDownloadStream()을 호출하면 Flux<DataBuffer>를 얻습니다.
    • 파일 삭제: delete(Query query)는 Mono<Void>를 반환합니다.

GridFS는 MongoDB 내에서 대용량 파일을 효율적으로 관리할 수 있는 강력한 솔루션입니다.

 

Aggregation Framework (집계 프레임워크)

MongoDB의 Aggregation Framework는 여러 단계(stage)를 파이프라인으로 연결하여 데이터를 변환하고 집계하는 강력한 도구입니다. Spring Data MongoDB는 이를 MongoTemplate (및 ReactiveMongoTemplate)과 @Aggregation 어노테이션을 통해 지원합니다.  

  • MongoTemplate.aggregate():
    • 집계 파이프라인을 Aggregation 객체로 정의합니다. Aggregation 클래스는 $match, $group, $project, $sort, $limit, $lookup 등 다양한 정적 팩토리 메서드를 제공하여 파이프라인 단계를 구성합니다.
    • mongoTemplate.aggregate(Aggregation aggregation, String inputCollectionName, Class<O> outputType) 메서드를 사용하여 집계를 실행합니다. outputType은 집계 결과가 매핑될 클래스입니다.
@Autowired
private MongoTemplate mongoTemplate;

public List<CategoryStats> getCategoryPriceStats() {
    Aggregation aggregation = Aggregation.newAggregation(
        Aggregation.group("category") // 카테고리별 그룹화
           .avg("price").as("averagePrice") // 평균 가격 계산
           .sum("price").as("totalPrice")   // 총 가격 계산
           .count().as("productCount"),    // 상품 개수 계산
        Aggregation.project("averagePrice", "totalPrice", "productCount") // 결과 필드 선택
           .and("category").previousOperation(), // 그룹 키(_id)를 category 필드로 변경
        Aggregation.sort(Sort.Direction.DESC, "productCount") // 상품 개수 내림차순 정렬
    );

    AggregationResults<CategoryStats> results = mongoTemplate.aggregate(
        aggregation, "products", CategoryStats.class // "products" 컬렉션 대상, CategoryStats 클래스로 결과 매핑
    );
    return results.getMappedResults();
}
// CategoryStats 클래스는 집계 결과를 담기 위한 DTO
public static class CategoryStats {
    String category;
    double averagePrice;
    double totalPrice;
    long productCount;
    // Getters, Setters...
}

 

  • @Aggregation 어노테이션:
    • Repository 메서드에 직접 집계 파이프라인을 JSON 문자열 배열 형태로 정의할 수 있습니다. 플레이스홀더(?0, ?1 등)를 사용하여 메서드 파라미터를 바인딩할 수 있습니다.  
public interface ProductRepository extends MongoRepository<Product, ObjectId> {
    @Aggregation(pipeline = {
        "{ '$match': { 'category':?0 } }",
        "{ '$group': { '_id': '$category', 'averagePrice': { '$avg': '$price' } } }",
        "{ '$project': { 'category': '$_id', 'averagePrice': 1, '_id': 0 } }"
    })
    List<CategoryAveragePrice> findAveragePriceByCategory(String category);
}
// CategoryAveragePrice는 결과를 담을 DTO 또는 인터페이스 기반 프로젝션
public static class CategoryAveragePrice {
    String category;
    double averagePrice;
    // Getters, Setters...
}
Aggregation Framework는 복잡한 데이터 분석 및 보고서 생성 요구사항을 효과적으로 처리할 수 있게 해줍니다.
 
 

Reactive 프로그래밍 모델 지원

Spring Data MongoDB는 ReactiveMongoRepository와 ReactiveMongoTemplate을 통해 완전한 리액티브 프로그래밍 모델을 지원합니다.  

  • 논블로킹 I/O: 리액티브 드라이버를 사용하여 데이터베이스 작업을 수행하므로 스레드가 I/O 작업을 기다리며 블로킹되지 않습니다. 이는 적은 수의 스레드로도 높은 동시성(concurrency)을 처리할 수 있게 하여 애플리케이션의 확장성과 자원 효율성을 크게 향상시킵니다.
  • 리액티브 타입 반환: 모든 Repository 및 Template 메서드는 Project Reactor의 Mono (0-1개 결과) 또는 Flux (0-N개 결과) 타입을 반환합니다. 이를 통해 리액티브 파이프라인을 구성하고 데이터 스트림을 비동기적으로 처리할 수 있습니다.
  • 아키텍처 영향: 리액티브 스택을 선택하는 것은 단순히 API 사용 방식을 바꾸는 것을 넘어, 애플리케이션 전체 아키텍처에 영향을 미칩니다. 데이터베이스 접근뿐만 아니라 웹 계층(예: Spring WebFlux) 등 다른 부분도 리액티브 방식으로 구성해야 그 효과를 극대화할 수 있습니다. 이는 높은 처리량과 응답성이 중요한 마이크로서비스나 실시간 데이터 처리 애플리케이션에 특히 적합합니다.  
     

리액티브 지원은 현대적인 고성능 애플리케이션 개발에 필수적인 요소로 자리 잡고 있으며, Spring Data MongoDB는 이를 위한 강력한 기반을 제공합니다.


참고 자료

https://spring.io/guides/gs/accessing-data-mongodb

 

Getting Started | Accessing Data with MongoDB

You can run the application from the command line with Gradle or Maven. You can also build a single executable JAR file that contains all the necessary dependencies, classes, and resources and run that. Building an executable jar makes it easy to ship, ver

spring.io

https://docs.spring.io/spring-data/mongodb/reference/

 

Spring Data MongoDB :: Spring Data MongoDB

Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.

docs.spring.io

반응형

'Spring' 카테고리의 다른 글

[Spring] Spring Data - JPA  (0) 2025.04.18
[Spring] Spring Data - JDBC  (0) 2025.04.17
[Spring] Annotation 기반 Bean 생명주기  (0) 2025.04.17