MongoDB Data: Document 클래스 Annotation 완벽 가이드
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의 역할을 이해하고, 프로젝트에 적절히 적용해보세요!