개요
MongoDB를 사용하면 _id 필드에 대하여 ObjectId를 사용하도록 권장하고 있지만, 다른 DB에서 데이터를 마이그레이션 하거나 데이터 구조상 _id의 값을 Number 형태의 값을 사용해야 하는 경우는 별도의 처리가 필요하다. JPA는 @GeneratedValue를 사용하여 Auto Increment Key 방식을 지원해 주지만 MongoDB의 경우에는 어떻게 처리해야 하는지 알아보자.
Getting started
프로젝트 설정
Spring Boot 프로젝트를 생성하면서 MongoDB 의존성을 추가해 준다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
......
}
상품 정보를 저장할 Product 객체를 추가한다.
@Document("product")
data class Product(val name: String, @Id var id: Long? = null)
application.properties 파일에 MongoDB URL을 설정한다.
spring.application.name=mongodb-auto-generated
spring.data.mongodb.uri={ URL }
Product 객체 저장 테스트
Product 객체를 생성하고 MongoTemplate을 사용하여 저장한다.
@SpringBootApplication
class MongodbAutoGeneratedApplication {
@Bean
fun run(mongoTemplate: MongoTemplate): ApplicationRunner =
ApplicationRunner { _ ->
val product = Product("iPhone")
mongoTemplate.save(product)
println(product)
}
}
fun main(args: Array<String>) {
runApplication<MongodbAutoGeneratedApplication>(*args)
}
애플리케이션을 실행해 보면 id 필드에 대하여 Long타입으로 변환이 불가능하여 오류가 발생한다.
org.springframework.dao.InvalidDataAccessApiUsageException:
Cannot autogenerate id of type java.lang.Long for entity of type org.igooo.mongo.autogenerated
SequenceGenerator 개발
SequenceGenerator 클래스를 사용하여 MongoDB에서 auto-generated field를 구현해 보자.
SequenceGenerator의 로직은 별도 Collection에 Sequence 번호를 저장하고, 객체가 저장될 때마다 Sequence 번호를 1씩 증가시켜서 해당 번호를 id로 사용하는 방식이다.
Sequence 정보를 저장할 객체를 생성한다.
@Document("database_seq")
data class DatabaseSeq(@Id val id: String, val seq: Long)
SequenceGenerator는 요청한 id 기준으로 seq 값을 1 증가시키고 해당 값을 리턴하여 id로 사용한다.
@Component
class SequenceGenerator(private val mongoTemplate: MongoTemplate) {
fun getNextSequence(id: String): Long = mongoTemplate.findAndModify(
Query(DatabaseSeq::id.isEqualTo(id)),
Update().inc(DatabaseSeq::seq.name, 1L),
options().returnNew(true).upsert(true),
DatabaseSeq::class.java
)?.seq ?: 1L
}
SequenceGenerator를 사용하여 MongoTemplate에 저장하기 전에 Product 객체에 id 값을 설정하여 저장한다.
@SpringBootApplication
class MongodbAutoGeneratedApplication {
@Bean
fun run(mongoTemplate: MongoTemplate, sequenceGenerator: SequenceGenerator): ApplicationRunner =
ApplicationRunner { _ ->
val product = Product("iPhone")
product.id = sequenceGenerator.getNextSequence("product")
mongoTemplate.save(product)
println(product)
}
}
fun main(args: Array<String>) {
runApplication<MongodbAutoGeneratedApplication>(*args)
}
실행 결과
Product(name=iPhone, id=1)
AbstractMongoEventListener를 사용하여 자동화하기
Product를 저장하는 로직에 id를 매번 설정하는 로직을 추가하는 것은 번거로운 작업이다. Spring에서 제공하는 AbstractMongoEventListener를 사용하면 저장하는 로직마다 id을 추가하는 로직을 제거할 수 있다.
AbstractMongoEventListener<Product> 객체를 상속받아 onBeforeConvert 메서드를 재구현하여 id 값을 설정한다.
@SpringBootApplication
class MongodbAutoGeneratedApplication {
@Bean
fun run(mongoTemplate: MongoTemplate): ApplicationRunner =
ApplicationRunner { _ ->
val product = Product("iPhone")
mongoTemplate.save(product)
println(product)
}
@Bean
fun productMongoEventListener(sequenceGenerator: SequenceGenerator): AbstractMongoEventListener<Product> =
object : AbstractMongoEventListener<Product>() {
override fun onBeforeConvert(event: BeforeConvertEvent<Product>) {
event.source.id = sequenceGenerator.getNextSequence("product")
}
}
}
fun main(args: Array<String>) {
runApplication<MongodbAutoGeneratedApplication>(*args)
}
실행 결과
Product(name=iPhone, id=2)
AbstractMongoEventListener<Product>를 사용해서 자동으로 Product 객체가 저장할 때 id가 설정되도록 한다.
Lifecycle Events
Spring의 MongoDB Mapping 프레임웍에는 ApplicationContext에 등록 가능한 이벤트를 지원한다.
위에 예제처럼 Product 객체가 org.bson.Document로 변환되기 전에 id 값을 설정하는 것처럼 처리하기 위해서는 AbstractMongoEventListener를 구현한 클래스를 Bean으로 등록하고 onBeforeConvert 메서드를 오버라이드하면 된다.
아래는 AbstractMappingEventListener의 callback 목록이다.
- onBeforeConvert: Called in MongoTemplate insert, insertList, and save operations before the object is converted to a Document by a MongoConverter.
- onBeforeSave: Called in MongoTemplate insert, insertList, and saveoperations before inserting or saving the Document in the database.
- onAfterSave: Called in MongoTemplate insert, insertList, and saveoperations after inserting or saving the Document in the database.
- onAfterLoad: Called in MongoTemplate find, findAndRemove, findOne, and getCollection methods after the Document has been retrieved from the database.
- onAfterConvert: Called in MongoTemplate find, findAndRemove, findOne, and getCollection methods after the Document has been retrieved from the database was converted to a POJO.
참고 : https://docs.spring.io/spring-data/mongodb/reference/mongodb/lifecycle-events.html
MongoTemplate에 doSave 메서드를 보면 데이터 처리 전 후로 등록된 Callback들을 호출하는 코드를 확인할 수 있다.
protected <T> T doSave(String collectionName, T objectToSave, MongoWriter<T> writer) {
objectToSave = (T)((BeforeConvertEvent)this.maybeEmitEvent(new BeforeConvertEvent(objectToSave, collectionName))).getSource();
objectToSave = (T)this.maybeCallBeforeConvert(objectToSave, collectionName);
EntityOperations.AdaptibleEntity<T> entity = this.operations.forEntity(objectToSave, this.mongoConverter.getConversionService());
entity.assertUpdateableIdIfNotSet();
MappedDocument mapped = entity.toMappedDocument(writer);
Document dbDoc = mapped.getDocument();
this.maybeEmitEvent(new BeforeSaveEvent(objectToSave, dbDoc, collectionName));
objectToSave = (T)this.maybeCallBeforeSave(objectToSave, dbDoc, collectionName);
Object id = this.saveDocument(collectionName, dbDoc, objectToSave.getClass());
T saved = (T)this.populateIdIfNecessary(objectToSave, id);
this.maybeEmitEvent(new AfterSaveEvent(saved, dbDoc, collectionName));
return (T)this.maybeCallAfterSave(saved, dbDoc, collectionName);
}
전체 소스코드는 GitHub에서 확인할 수 있습니다.
'dev > spring' 카테고리의 다른 글
Spring Boot ShedLock in Action (with MongoDB) (0) | 2025.03.18 |
---|---|
Spring AI를 사용하여 당근 상품 등록 기능 개발하기 (0) | 2025.02.16 |
Spring boot devtools (1) | 2025.02.07 |
Spring AI 영수증 이미지 처리하기 (0) | 2025.01.21 |
Spring Boot + Testcontainers 테스트 빠르게 실행하기 (1) | 2025.01.16 |