Spring Boot 3에서 MongoDB와 연동할 때 Document 클래스에서 사용할 수 있는 다양한 Annotation에 대해 설명하고, 각 Annotation의 역할과 샘플 코드를 제공합니다. 이 글은 초보자도 쉽게 이해할 수 있도록 작성되었습니다.


Spring Boot 3와 MongoDB Data: Document 클래스 Annotation 완벽 가이드

Spring Boot 3와 Spring Data MongoDB를 활용하면 손쉽게 MongoDB와 연동할 수 있습니다. 특히, 도메인 객체(엔티티)에 다양한 Annotation을 활용하여 데이터의 구조와 동작을 세밀하게 제어할 수 있습니다. 이 글에서는 Document 클래스에서 사용할 수 있는 주요 Annotation 종류와 각각의 역할, 그리고 실전에서 바로 쓸 수 있는 샘플 코드를 제공합니다.

1. @Document

설명

  • 이 Annotation은 해당 클래스가 MongoDB의 컬렉션과 매핑된다는 것을 명시합니다.
  • 컬렉션 이름을 지정하지 않으면 클래스 이름이 컬렉션 이름으로 사용됩니다.

샘플 코드

import org.springframework.data.mongodb.core.mapping.Document

@Document(collection = "users")
data class User(
    val id: String? = null,
    val name: String,
    val email: String
)

2. @Id

설명

  • MongoDB의 _id 필드와 매핑되는 필드에 사용합니다.
  • 기본적으로 String, ObjectId, Long 등 다양한 타입을 사용할 수 있습니다.

샘플 코드

import org.springframework.data.annotation.Id

@Document(collection = "products")
data class Product(
    @Id
    val id: String? = null,
    val name: String,
    val price: Double
)

3. @Field

설명

  • 도메인 객체의 필드와 MongoDB의 컬렉션 필드명을 다르게 매핑할 때 사용합니다.
  • 주로 변수명과 실제 DB 필드명이 다를 때 유용합니다.

샘플 코드

import org.springframework.data.mongodb.core.mapping.Field

@Document(collection = "orders")
data class Order(
    @Id
    val id: String? = null,
    @Field("order_no")
    val orderNumber: String,
    val amount: Int
)

4. @Transient

설명

  • 해당 필드는 MongoDB에 저장되지 않습니다.
  • 계산값, 임시 데이터 등 DB에 저장할 필요 없는 값에 사용합니다.

샘플 코드

import org.springframework.data.annotation.Transient

@Document(collection = "sessions")
data class Session(
    @Id
    val id: String? = null,
    val userId: String,
    @Transient
    val tempToken: String? = null
)

5. @DBRef

설명

  • @DBRef는 다른 컬렉션의 Document(엔티티)와 참조(Reference) 관계를 맺을 때 사용합니다.
  • MongoDB의 Reference는 RDB의 Join과는 다르며, 실제로는 참조된 Document의 ID만 저장됩니다.
  • Spring Data MongoDB는 @DBRef가 붙은 필드를 조회할 때 해당 참조 객체를 자동으로 불러옵니다.
  • lazy = true 옵션을 사용하면 지연로딩(Lazy Loading)이 가능합니다.
  • 단방향/양방향 참조, null 허용, 컬렉션 타입(List/Set 등)도 지원합니다.
  • 실제로는 참조 무결성 보장이 없으므로, 데이터 삭제/수정 시 주의해야 합니다.

샘플 코드: 다양한 관계 예시

1) 단방향 참조

import org.springframework.data.mongodb.core.mapping.DBRef
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.annotation.Id

@Document(collection = "users")
data class User(
    @Id
    val id: String? = null,
    val name: String
)

@Document(collection = "comments")
data class Comment(
    @Id
    val id: String? = null,
    val content: String,
    @DBRef
    val user: User
)

2) 양방향 참조

@Document(collection = "posts")
data class Post(
    @Id
    val id: String? = null,
    val title: String,
    @DBRef
    val comments: List<Comment> = listOf()
)

@Document(collection = "comments")
data class Comment(
    @Id
    val id: String? = null,
    val content: String,
    @DBRef
    val post: Post? = null
)

3) Lazy Loading (지연 로딩)

@Document(collection = "orders")
data class Order(
    @Id
    val id: String? = null,
    @DBRef(lazy = true)
    val user: User? = null
)

4) null 허용 및 컬렉션 참조

@Document(collection = "groups")
data class Group(
    @Id
    val id: String? = null,
    val name: String,
    @DBRef
    val members: List<User>? = null
)

참고 사항

  • @DBRef는 편리하지만, 대량의 데이터나 복잡한 관계에서는 성능 저하가 발생할 수 있습니다.
  • MongoDB의 설계 철학은 중첩(embedding)과 참조(reference)를 상황에 맞게 혼용하는 것이므로, 실제 서비스에서는 데이터 구조에 맞는 방식을 선택해야 합니다.

6. @Indexed, @CompoundIndex, @TextIndexed

설명 및 사용 시점

  • 인덱스는 데이터베이스에서 특정 필드나 조합에 대해 빠른 검색을 가능하게 하며, 대용량 데이터에서 성능을 크게 향상시킵니다. Spring Data MongoDB는 Annotation을 통해 인덱스를 손쉽게 선언할 수 있습니다.

1) @Indexed

  • 사용 시점:
    • 단일 필드에 대해 자주 검색(조회, 정렬, 조건 검색 등)이 이루어질 때 사용합니다.
    • 예: 이메일, 사용자명, 상품코드 등 중복이 없어야 하거나, 빠른 단일 검색이 필요한 필드.
    • unique = true 옵션으로 유일성(중복 방지)도 보장할 수 있습니다.
  • 주의: 인덱스가 많아질수록 쓰기 성능은 저하될 수 있으므로 꼭 필요한 필드에만 사용하세요.

2) @CompoundIndex

  • 사용 시점:
    • 여러 필드를 조합해서 자주 검색하거나, 특정 조건(예: 사용자 + 날짜, 카테고리 + 상태 등)으로 복합 조회가 필요한 경우에 사용합니다.
    • 단일 필드 인덱스만으로는 성능이 부족할 때, 두 개 이상의 필드를 묶어 복합 인덱스를 생성합니다.
    • unique = true로 복합 유일성 제약도 가능.
  • 주의: 복합 인덱스의 필드 순서가 쿼리 성능에 영향을 미치므로, 실제 쿼리 패턴을 고려해 설계해야 합니다.

3) @TextIndexed

  • 사용 시점:
    • 텍스트 검색(예: 게시글 본문, 상품 설명, 댓글 등)에서 키워드 기반의 검색을 지원하고 싶을 때 사용합니다.
    • 여러 필드에 동시에 적용할 수 있으며, MongoDB의 $text 쿼리와 함께 사용됩니다.
    • 뉴스, 블로그, 검색 서비스 등에서 유용합니다.
  • 주의: 텍스트 인덱스는 대량의 텍스트 데이터에 적합하며, 숫자/날짜 등에는 적합하지 않습니다.

샘플 코드 및 상황별 예시

import org.springframework.data.mongodb.core.index.Indexed
import org.springframework.data.mongodb.core.index.CompoundIndex
import org.springframework.data.mongodb.core.index.TextIndexed
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.annotation.Id

// 1) @Indexed: 이메일 중복 방지 및 빠른 조회
@Document(collection = "users")
data class User(
    @Id
    val id: String? = null,
    @Indexed(unique = true)
    val email: String,
    val name: String
)

// 2) @CompoundIndex: 사용자 + 날짜 조합의 복합 인덱스
@Document(collection = "orders")
@CompoundIndex(def = "{'userId': 1, 'orderDate': -1}")
data class Order(
    @Id
    val id: String? = null,
    val userId: String,
    val orderDate: String,
    val amount: Int
)

// 3) @TextIndexed: 게시글 본문 검색
@Document(collection = "articles")
data class Article(
    @Id
    val id: String? = null,
    val title: String,
    @TextIndexed
    val content: String,
    val author: String
)

참고 및 활용 팁

  • 인덱스는 검색 성능을 높이지만, 인덱스가 많을수록 데이터 저장/수정/삭제 시 오버헤드가 발생할 수 있습니다.
  • 복합 인덱스는 실제 쿼리에서 자주 사용하는 필드 조합과 순서를 기준으로 설계하세요.
  • 텍스트 인덱스는 자연어 검색, 키워드 검색 등에서 강력한 성능을 발휘합니다.

7. @PersistenceConstructor

설명

  • MongoDB Document를 객체로 변환할 때 사용할 생성자를 지정합니다.
  • 여러 생성자가 있을 때 어떤 생성자를 사용할지 명확히 할 수 있습니다.

샘플 코드

import org.springframework.data.annotation.PersistenceConstructor

@Document(collection = "logs")
data class Log @PersistenceConstructor constructor(
    val id: String? = null,
    val message: String,
    val level: String
)

8. 기타 Annotation

  • @CreatedDate, @LastModifiedDate: 생성/수정 시각 자동 기록
  • @Version: 낙관적 락(Optimistic Lock) 지원
  • @GeoSpatialIndexed: 위치 기반 인덱스

@Version(낙관적 락) 자세히 알아보기

개념 및 동작 방식

  • @Version은 동시성 문제를 방지하기 위한 낙관적 락(Optimistic Lock)을 지원하는 Annotation입니다.
  • 여러 사용자가 동시에 같은 Document를 수정할 때, 마지막에 저장하는 사용자의 변경만 반영되고, 그 사이에 값이 바뀌었으면 예외가 발생해 데이터 충돌을 방지합니다.
  • Spring Data MongoDB는 @Version 필드가 있는 경우, 저장 시점에 해당 값이 일치하는지 확인하고, 다르면 OptimisticLockingFailureException을 발생시킵니다.
  • 버전 필드는 일반적으로 Long 타입을 사용하며, Document가 갱신될 때마다 자동으로 1씩 증가합니다.

장점 및 주의점

  • 장점: 데이터 일관성 보장, 동시성 환경에서 안전하게 업데이트 가능, 별도의 락 테이블이나 DB 락 필요 없음.
  • 주의: 충돌이 자주 발생하면 사용자 경험이 나빠질 수 있으므로, 대량 동시 수정이 적은 데이터에 적합합니다. 충돌 발생 시 적절한 예외 처리와 사용자 안내가 필요합니다.

샘플 코드 및 실전 예시

import org.springframework.data.annotation.Version
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.annotation.Id

@Document(collection = "accounts")
data class Account(
    @Id
    val id: String? = null,
    val owner: String,
    var balance: Int,
    @Version
    val version: Long? = null
)
// 서비스 계층에서 동시 업데이트 예시
fun deposit(accountId: String, amount: Int) {
    val account = accountRepository.findById(accountId).orElseThrow()
    account.balance += amount
    try {
        accountRepository.save(account)
    } catch (e: org.springframework.dao.OptimisticLockingFailureException) {
        // 다른 사용자가 먼저 수정한 경우 예외 발생
        // 사용자에게 재시도 안내 등 처리 필요
        throw RuntimeException("동시 수정 충돌이 발생했습니다. 다시 시도해주세요.")
    }
}
  • 위 예시에서 두 사용자가 동시에 잔액을 수정하면, 먼저 저장한 쪽만 성공하고, 나중에 저장하는 쪽은 예외가 발생합니다.
  • 실무에서는 충돌 시 재시도 로직, 사용자 알림, 변경 내용 병합 정책 등을 함께 고려해야 합니다.

기타 샘플 코드

import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.mongodb.core.index.GeoSpatialIndexed
import java.time.LocalDateTime

@Document(collection = "places")
data class Place(
    @Id
    val id: String? = null,
    val name: String,
    @GeoSpatialIndexed
    val location: List<Double>,
    @CreatedDate
    val createdAt: LocalDateTime? = null,
    @LastModifiedDate
    val updatedAt: LocalDateTime? = null
)

Annotation 조합 예시

실제 프로젝트에서는 여러 Annotation을 조합하여 사용할 수 있습니다. 아래는 복합적으로 Annotation을 적용한 예시입니다.

@Document(collection = "members")
@CompoundIndex(def = "{'email': 1, 'nickname': 1}", unique = true)
data class Member(
    @Id
    val id: String? = null,
    @Indexed(unique = true)
    val email: String,
    @Field("nick")
    val nickname: String,
    @CreatedDate
    val joinedAt: LocalDateTime? = null,
    @LastModifiedDate
    val updatedAt: LocalDateTime? = null
)

참고할 만한 공식 레퍼런스 및 자료


Spring Data MongoDB의 다양한 Annotation을 활용하면, MongoDB와의 데이터 매핑 및 관리가 훨씬 쉬워집니다. 각 Annotation의 역할을 이해하고, 프로젝트에 적절히 적용해보세요!