Spring Data MongoDB란?
Spring Data MongoDB는 MongoDB를 Spring 생태계에서 Repository 패턴과 Template API로 사용할 수 있게 해주는 모듈입니다. JPA와 유사한 인터페이스를 제공하면서도 MongoDB의 도큐먼트 모델, 유연한 스키마, 집계 파이프라인 등 NoSQL 고유 기능을 그대로 활용할 수 있습니다. RDB로는 처리하기 어려운 비정형 데이터, 이벤트 로그, 카탈로그 등에 적합합니다.
의존성과 기본 설정
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
# application.yml
spring:
data:
mongodb:
uri: mongodb://user:password@localhost:27017/mydb?authSource=admin
# 또는 개별 설정
host: localhost
port: 27017
database: mydb
username: user
password: password
auto-index-creation: true # 개발 환경에서만 true
도큐먼트 매핑: @Document
JPA의 @Entity 대신 @Document를 사용합니다. MongoDB의 유연한 스키마를 활용한 도큐먼트 설계:
@Document(collection = "products")
@CompoundIndex(name = "category_name_idx",
def = "{'category': 1, 'name': 1}")
public class Product {
@Id
private String id; // MongoDB ObjectId 자동 생성
@Indexed(unique = true)
private String sku;
@Field("product_name") // MongoDB 필드명 매핑
private String name;
private String category;
private BigDecimal price;
// 내장 도큐먼트 (Embedded)
private List<Variant> variants;
// 동적 속성 (스키마리스 활용)
private Map<String, Object> attributes;
@CreatedDate
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;
@Version
private Long version; // Optimistic Locking
}
// 내장 도큐먼트 (별도 컬렉션 아님)
public class Variant {
private String color;
private String size;
private int stock;
private BigDecimal priceAdjustment;
}
주요 어노테이션:
| 어노테이션 | 설명 |
|---|---|
| @Document | 컬렉션 매핑 (JPA의 @Entity) |
| @Id | _id 필드 매핑. String이면 ObjectId 자동 생성 |
| @Field | MongoDB 필드명 지정 |
| @Indexed | 단일 필드 인덱스 |
| @CompoundIndex | 복합 인덱스 |
| @TextIndexed | 텍스트 검색 인덱스 |
| @DBRef | 다른 컬렉션 참조 (JOIN과 유사) |
| @Version | Optimistic Locking |
MongoRepository: 기본 CRUD와 쿼리 메서드
public interface ProductRepository
extends MongoRepository<Product, String> {
// 메서드 이름으로 쿼리 자동 생성
List<Product> findByCategoryAndPriceLessThan(
String category, BigDecimal price);
// 내장 도큐먼트 필드 쿼리
List<Product> findByVariantsColor(String color);
// 정렬 + 페이징
Page<Product> findByCategoryOrderByPriceDesc(
String category, Pageable pageable);
// @Query로 MongoDB 네이티브 쿼리
@Query("{ 'category': ?0, 'price': { $gte: ?1, $lte: ?2 } }")
List<Product> findByCategoryAndPriceRange(
String category, BigDecimal min, BigDecimal max);
// 프로젝션: 필요한 필드만 조회
@Query(value = "{ 'category': ?0 }",
fields = "{ 'name': 1, 'price': 1 }")
List<Product> findNameAndPriceByCategory(String category);
// 존재 여부 확인
boolean existsBySku(String sku);
// 삭제
long deleteByCategory(String category);
// 텍스트 검색
@TextScore
List<Product> findByNameContaining(String keyword);
}
MongoTemplate: 복잡한 쿼리
Repository만으로는 부족할 때 MongoTemplate을 직접 사용합니다:
@Service
@RequiredArgsConstructor
public class ProductQueryService {
private final MongoTemplate mongoTemplate;
// 동적 조건 쿼리
public List<Product> search(ProductSearchCriteria criteria) {
Query query = new Query();
if (criteria.getCategory() != null) {
query.addCriteria(
Criteria.where("category").is(criteria.getCategory()));
}
if (criteria.getMinPrice() != null) {
query.addCriteria(
Criteria.where("price").gte(criteria.getMinPrice()));
}
if (criteria.getMaxPrice() != null) {
query.addCriteria(
Criteria.where("price").lte(criteria.getMaxPrice()));
}
if (criteria.getColors() != null) {
query.addCriteria(
Criteria.where("variants.color")
.in(criteria.getColors()));
}
// 페이징 + 정렬
query.with(PageRequest.of(
criteria.getPage(), criteria.getSize(),
Sort.by(Sort.Direction.DESC, "createdAt")));
return mongoTemplate.find(query, Product.class);
}
// 부분 업데이트 (전체 도큐먼트 교체 아닌 필드만 수정)
public void updatePrice(String sku, BigDecimal newPrice) {
Query query = Query.query(Criteria.where("sku").is(sku));
Update update = new Update()
.set("price", newPrice)
.set("updatedAt", Instant.now())
.inc("version", 1);
mongoTemplate.updateFirst(query, update, Product.class);
}
// Upsert: 있으면 수정, 없으면 삽입
public void upsertProduct(String sku, Product product) {
Query query = Query.query(Criteria.where("sku").is(sku));
Update update = new Update()
.set("name", product.getName())
.set("price", product.getPrice())
.set("category", product.getCategory())
.setOnInsert("createdAt", Instant.now());
mongoTemplate.upsert(query, update, Product.class);
}
// 배열 조작: 내장 도큐먼트 추가/제거
public void addVariant(String productId, Variant variant) {
Query query = Query.query(Criteria.where("id").is(productId));
Update update = new Update().push("variants", variant);
mongoTemplate.updateFirst(query, update, Product.class);
}
public void removeVariant(String productId, String color) {
Query query = Query.query(Criteria.where("id").is(productId));
Update update = new Update().pull("variants",
Query.query(Criteria.where("color").is(color)));
mongoTemplate.updateFirst(query, update, Product.class);
}
}
Aggregation Pipeline: 집계 쿼리
MongoDB의 핵심 기능인 집계 파이프라인을 Spring에서 활용하는 방법:
@Service
@RequiredArgsConstructor
public class ProductAnalyticsService {
private final MongoTemplate mongoTemplate;
// 카테고리별 평균 가격·상품 수
public List<CategoryStats> getCategoryStats() {
Aggregation agg = Aggregation.newAggregation(
Aggregation.group("category")
.count().as("productCount")
.avg("price").as("avgPrice")
.min("price").as("minPrice")
.max("price").as("maxPrice"),
Aggregation.sort(Sort.Direction.DESC, "productCount"),
Aggregation.project()
.and("_id").as("category")
.andInclude("productCount", "avgPrice",
"minPrice", "maxPrice")
);
return mongoTemplate
.aggregate(agg, "products", CategoryStats.class)
.getMappedResults();
}
// Unwind + Group: 변형(variants)별 재고 합계
public List<ColorStockStats> getStockByColor() {
Aggregation agg = Aggregation.newAggregation(
Aggregation.unwind("variants"),
Aggregation.group("variants.color")
.sum("variants.stock").as("totalStock")
.count().as("variantCount"),
Aggregation.match(
Criteria.where("totalStock").gt(0)),
Aggregation.sort(Sort.Direction.DESC, "totalStock")
);
return mongoTemplate
.aggregate(agg, "products", ColorStockStats.class)
.getMappedResults();
}
// Lookup: 다른 컬렉션 JOIN
public List<ProductWithReviews> getProductsWithReviews(
String category) {
Aggregation agg = Aggregation.newAggregation(
Aggregation.match(
Criteria.where("category").is(category)),
Aggregation.lookup(
"reviews", // 조인할 컬렉션
"_id", // 로컬 필드
"productId", // 외래 필드
"reviews"), // 결과 필드명
Aggregation.addFields()
.addFieldWithValue("reviewCount",
new Document("$size", "$reviews"))
.build(),
Aggregation.sort(Sort.Direction.DESC, "reviewCount")
);
return mongoTemplate
.aggregate(agg, "products", ProductWithReviews.class)
.getMappedResults();
}
}
인덱스 전략
MongoDB 성능의 핵심은 인덱스입니다. 프로덕션에서는 auto-index-creation: false로 설정하고 마이그레이션으로 인덱스를 관리하세요:
@Configuration
public class MongoIndexConfig {
@Autowired
private MongoTemplate mongoTemplate;
@EventListener(ApplicationReadyEvent.class)
public void ensureIndexes() {
IndexOperations ops = mongoTemplate
.indexOps(Product.class);
// TTL 인덱스: 30일 후 자동 삭제
ops.ensureIndex(new Index()
.on("createdAt", Sort.Direction.ASC)
.expire(Duration.ofDays(30))
.named("ttl_created_at"));
// 부분 인덱스: 특정 조건의 도큐먼트만 인덱싱
ops.ensureIndex(new Index()
.on("price", Sort.Direction.ASC)
.partial(PartialIndexFilter.of(
Criteria.where("category").is("electronics")))
.named("price_electronics_partial"));
// 텍스트 인덱스
ops.ensureIndex(new TextIndexDefinition.TextIndexDefinitionBuilder()
.onField("name", 10F) // 가중치 10
.onField("category", 5F) // 가중치 5
.named("text_search_idx")
.build());
}
}
트랜잭션 지원 (Replica Set 필수)
MongoDB 4.0+에서 멀티 도큐먼트 트랜잭션을 지원합니다. Spring Transaction과 동일한 @Transactional을 사용합니다:
@Configuration
public class MongoTransactionConfig {
@Bean
MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
}
@Service
@Transactional
public class OrderService {
public Order placeOrder(CreateOrderDto dto) {
// 재고 차감
productRepository.decrementStock(dto.getProductId(), dto.getQty());
// 주문 생성
Order order = orderRepository.save(new Order(dto));
// 결제 기록
paymentRepository.save(new Payment(order));
return order;
}
}
Change Streams: 실시간 변경 감지
@Component
public class ProductChangeListener {
@Autowired
private MongoTemplate mongoTemplate;
@EventListener(ApplicationReadyEvent.class)
public void watchProducts() {
MessageListenerContainer container =
new DefaultMessageListenerContainer(mongoTemplate);
container.start();
ChangeStreamRequest<Product> request =
ChangeStreamRequest.builder()
.collection("products")
.filter(Aggregation.newAggregation(
Aggregation.match(Criteria.where("operationType")
.in("insert", "update"))))
.publishTo(message -> {
Product product = message.getBody();
log.info("Product changed: {}", product.getSku());
// 캐시 무효화, 알림 전송 등
})
.build();
container.register(request, Product.class);
}
}
마치며
Spring Data MongoDB는 Repository의 간결함과 MongoTemplate의 유연함을 모두 제공합니다. 도큐먼트 설계에서 내장 도큐먼트를 적극 활용하여 JOIN을 최소화하고, Aggregation Pipeline으로 복잡한 집계를 처리하세요. 인덱스 전략(TTL, 부분, 텍스트)을 정확히 설계하고, 프로덕션에서는 auto-index-creation을 끄고 마이그레이션으로 관리하는 것이 핵심입니다. Spring Data Redis와 함께 캐시 계층을 구성하면 더욱 강력한 데이터 아키텍처를 구축할 수 있습니다.