모종닷컴

Mongock (with Spring Boot) 본문

Programming/Spring

Mongock (with Spring Boot)

모종 2022. 11. 26. 21:02
반응형

MySQL의 경우 flyway를 통해서 데이터 마이그레이션을 진행할 수 있는데 Mongodb의 경우는 데이터 마이그레이션을 하기 위해서는 어떤 라이브러리를 사용할 수 있을까?

Mongock

https://mongock.io/

페이지에서 소개하는 간단한 특징 몇 개만 짚어보자면 아래와 같다.

  • 자바 기반 마이그레이션 도구
  • 견고한 락 메커니즘을 이용하기 때문에 분산 환경에서 안정적으로 실행 가능하다.
  • 코드로 마이그레이션 스크립트를 작성할 수 있다.
  • MongoDB 마이그레이션이 가능하며 (자칭) 가장 안정적인 프로덕트
  • 오픈 소스
  • 정기적인 유지보수
  • Spring과 호환이 좋다.

노란줄만 보더라도 적합해 보이는 툴입니다. MongoDB 마이그레이션으로 가장 많은 스타를 받고 있는 mongobee의 경우 업데이트가 이제는 거의 없는 반면 Mongock은 비교적 원활하게 업데이트가 꾸준히 이루어지고 있습니다.

스프링 적용

스프링에 mongock를 적용해보기 위해 프로젝트를 하나 만들었습니다. kotlin + Spring Boot입니다. build.gradle.kts의 dependencies는 아래와 같이 추가해주었습니다.

build.gradle.kts 

implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("io.mongock:mongodb-springdata-v3-driver:5.1.6")
implementation("io.mongock:mongock-springboot:5.1.6")
implementation("io.github.microutils:kotlin-logging:3.0.4")
developmentOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")

Mongodb

Post.kt

@Document(collection = Post.COLLECTION_NAME)
class Post(
    @Id var id: Long,
    var title: String,
    var content: String,
    @field:CreatedDate
    var createdAt: LocalDateTime = LocalDateTime.now(),
    @field:LastModifiedBy
    var updatedAt: LocalDateTime = LocalDateTime.now()
) {
    companion object {
        const val COLLECTION_NAME = "posts"
    }
}

PostRepository.kt

@Repository(Post.COLLECTION_NAME)
interface PostRepository : MongoRepository<Post, Long>

MongoConstants.kt

object MongoConstants {
    const val TX_MANAGER_BEAN_NAME = "mongoTransactionManager"
    const val REPLICATION_SET_PROPERTY = "spring.data.mongodb.replSet"
}

MongoDBConfig.kt

@Configuration
@EnableMongoAuditing
@EnableMongoRepositories(basePackageClasses = [PostRepository::class])
class MongoDBConfig {
    @Bean(name = [MongoConstants.TX_MANAGER_BEAN_NAME])
    @ConditionalOnProperty(value = [MongoConstants.REPLICATION_SET_PROPERTY], havingValue = "true")
    fun mongoTxManger(factory: MongoDatabaseFactory) = MongoTransactionManager(
        factory,
        TransactionOptions.builder()
            .readConcern(ReadConcern.LOCAL)
            .build()
    )
}

Property

MongoDB 트랜잭션을 사용하기 위해서는 replication 구성이 되어있어야 하는데 혹시 되어있지 않다면 Spring MongoDB Transaction Support 글을 읽어주세요.

spring:
  data:
    mongodb:
      uri: mongodb://localhost:30000/test?replicaSet=replset&readPreference=primary
      replSet: true

Mongock 

Mongock 설정은 대부분 외부화된 프로퍼티 파일에 정의만 하면되고, @EnableMongock만 정의해주면 됩니다.

@Configuration
@EnableMongock
class MongockConfig

mongock 관련 프로퍼티는 아래와 같이 사용하였습니다.

mongock:
  migration-scan-package:
    - com.example.practice.mongock.migration
  transaction-enabled: true

migration-scan-package = 마이그레이션 코드를 저장하고 있는 패키지위치를 설정해주면 됩니다. 저의 경우 마이그레이션 파일들을 com.example.practice.mongock.migration 패키지 안에 모아놓을 예정이므로 위처럼 설정하였습니다.

transaction-enabled = Mongock의 Runner가 패키지의 파일을 읽은 후 마이그레이션을 진행할 텐데 해당 마이그레이션 시 트랜잭션으로 묶어서 실행하지에 대한 설정입니다. 필요한 경우 파일마다 트랜잭션에서 실행할지 등의 설정을 할 수 있으니 default는 true로 두었습니다.

마이그레이션 파일 생성하기

마이그레이션 파일을 만들때에는 클래스 어노테이션으로 ChangeUnit을 사용하면 되고, 내부에 각각 @Execution, @RollbackExecution를 지닌 메서드를 만들어주면 됩니다. 그러면 Mongock Runner가 ChangeUnit이 붙은 파일을 로드하고 ChangeUnit의 order 순서대로 각 파일을 실행할 겁니다. 물론 이미 적용된 파일은 다시 실행되지 않을 겁니다.

https://docs.mongock.io/v5/technical-overview/index.html

파일을 총 3개를 만들어볼 예정인데요.

  1. Post 컬렉션을 생성 & 초기 Post 데이터 세팅.
  2. 추가 Post 데이터 세팅.
  3. Post 컬렉션에 인덱스 추가

이 순으로 마이그레이션 파일을 만들고 실제로 몽고에도 잘 적용되었는지 확인해보겠습니다.

PostInitializer.kt

/**
 * - post 컬렉션 생성
 * - 초기 데이터 삽입
 * */
@ChangeUnit(id="post-collection-init", order = "1", author = "mojong")
class PosInitializer(
    private val mongoTemplate: MongoTemplate,
    private val postRepository: PostRepository
) {
    @BeforeExecution
    fun init() {
        mongoTemplate.createCollection(Post.COLLECTION_NAME)
    }

    @RollbackBeforeExecution
    fun rollbackBeforeExecution() {
        mongoTemplate.dropCollection(Post.COLLECTION_NAME)
    }

    private val target = listOf(
        Post(id = 1, title = "제목1", content = "내용1"),
        Post(id = 2, title = "제목2", content = "내용2")
    )

    @Execution
    fun execution(){
        postRepository.saveAll(target)
    }

    @RollbackExecution
    fun rollbackExecution() {
        postRepository.deleteAll(target)
    }
}

InsertPost.kt

/**
 * - Post 컬렉션에 추가 데이터 삽입
 * */
@ChangeUnit(id = "insert-post-data", order = "2", author = "mojong")
class InsertPost(
    private val mongoTemplate: MongoTemplate
) {
    @Execution
    fun addPost() {
        val post = Post(id = 3, title = "제목3", content = "내용3")
        mongoTemplate.insert(post)
    }

    @RollbackExecution
    fun rollback() {
        val post = Post(id = 3, title = "제목3", content = "내용3")
        mongoTemplate.remove(post)
    }
}

CreatePostIndex.kt

/**
 * - post (title) 인덱스 생성
 * */
@ChangeUnit(id = "create-title-index", order = "3", author = "mojong", transactional = false)
class CreatePostIndex(
    private val mongoTemplate: MongoTemplate
) {
    private val idxName = "idx_title"
    @Execution
    fun createTitleIndex() {
        mongoTemplate.indexOps(Post::class.java).ensureIndex(Index("title", Sort.Direction.ASC).named(idxName))
    }

    @RollbackExecution
    fun rollback() {
        mongoTemplate.indexOps(Post::class.java).dropIndex(idxName)
    }
}

※ MongoDB가 트랜잭션 내에서 인덱스를 만드는 것을 제한해서 위의 경우 @ChangeUnit의 transactional을 false로 설정하였습니다. 

테스트

이제 준비가 되었으니 한번 애플리케이션을 실행해보도록 하겠습니다. 로그를 보니 듬성듬성 Mongock이 일을 하고 있습니다.

Mongock runner COMMUNITY version[5.1.6]
Running Mongock with NO metadata
...
Mongock trying to acquire the lock
Mongock acquired the lock until: 
Starting mongock lock daemon...
...
Mongock starting the data migration sequence id
...
method[com.example.practice.mongock.migration.PosInitializer] with arguments: [org.springframework.data.mongodb.core.MongoTemplate, com.sun.proxy.$Proxy78]
PASSED OVER - {"id"="post-collection-init_before", "type"="before-execution", "author"="mojong", "class"="PosInitializer", "method"="init"}
PASSED OVER - {"id"="post-collection-init", "type"="execution", "author"="mojong", "class"="PosInitializer", "method"="execution"}
method[com.example.practice.mongock.migration.InsertPost] with arguments: [org.springframework.data.mongodb.core.MongoTemplate]
PASSED OVER - {"id"="insert-post-data", "type"="execution", "author"="mojong", "class"="InsertPost", "method"="addPost"}
method[com.example.practice.mongock.migration.CreatePostIndex] with arguments: [org.springframework.data.mongodb.core.MongoTemplate]
method[createTitleIndex] with arguments: []
APPLIED - {"id"="create-title-index", "type"="execution", "author"="mojong", "class"="CreatePostIndex", "method"="createTitleIndex"}
Mongock releasing the lock
Mongock releasing the lock
Mongock released the lock
Mongock has finished

애플리케이션이 정상적으로 떴으니 몽고에서도 한번 확인을 해보도록 하겠습니다.

replset:PRIMARY> posts.find({});
{ "_id" : NumberLong(1), "title" : "제목1", "content" : "내용1", "createdAt" : ISODate("2022-11-26T06:30:25.882Z"), "updatedAt" : ISODate("2022-11-26T06:30:25.882Z"), "_class" : "com.example.practice.post.Post" }
{ "_id" : NumberLong(2), "title" : "제목2", "content" : "내용2", "createdAt" : ISODate("2022-11-26T06:30:25.882Z"), "updatedAt" : ISODate("2022-11-26T06:30:25.882Z"), "_class" : "com.example.practice.post.Post" }
{ "_id" : NumberLong(3), "title" : "제목3", "content" : "내용3", "createdAt" : ISODate("2022-11-26T06:30:25.957Z"), "updatedAt" : ISODate("2022-11-26T06:30:25.957Z"), "_class" : "com.example.practice.post.Post" }

포스트 데이터 3개가 잘 들어가 있습니다.

replset:PRIMARY> var posts = db.getCollection("posts")
replset:PRIMARY> posts.getIndexes();
[
	{
		"v" : 2,
		"key" : {
			"_id" : 1
		},
		"name" : "_id_"
	},
	{
		"v" : 2,
		"key" : {
			"title" : 1
		},
		"name" : "idx_title"
	}
]

title 인덱스도 잘 만들어졌습니다.

replset:PRIMARY> var cklog = db.getCollection("mongockChangeLog")
replset:PRIMARY> cklog.find({})
{ "_id" : ObjectId("6381b28130aa30888c87df71"), "author" : "mojong", "changeId" : "post-collection-init_before", "executionId" : "2022-11-26T15:30:25.575-738", "changeLogClass" : "com.example.practice.mongock.migration.PosInitializer", "changeSetMethod" : "init", "errorTrace" : null, "executionHostname" : "johnnyui-MacBookPro.local", "executionMillis" : NumberLong(15), "metadata" : null, "state" : "EXECUTED", "timestamp" : ISODate("2022-11-26T06:30:25.898Z"), "type" : "BEFORE_EXECUTION" }
{ "_id" : ObjectId("6381b28130aa30888c87df7e"), "author" : "mojong", "changeId" : "post-collection-init", "executionId" : "2022-11-26T15:30:25.575-738", "changeLogClass" : "com.example.practice.mongock.migration.PosInitializer", "changeSetMethod" : "execution", "errorTrace" : null, "executionHostname" : "johnnyui-MacBookPro.local", "executionMillis" : NumberLong(36), "metadata" : null, "state" : "EXECUTED", "timestamp" : ISODate("2022-11-26T06:30:25.947Z"), "type" : "EXECUTION" }
{ "_id" : ObjectId("6381b28130aa30888c87df88"), "author" : "mojong", "changeId" : "insert-post-data", "executionId" : "2022-11-26T15:30:25.575-738", "changeLogClass" : "com.example.practice.mongock.migration.InsertPost", "changeSetMethod" : "addTitleIndex", "errorTrace" : null, "executionHostname" : "johnnyui-MacBookPro.local", "executionMillis" : NumberLong(5), "metadata" : null, "state" : "EXECUTED", "timestamp" : ISODate("2022-11-26T06:30:25.963Z"), "type" : "EXECUTION" }
{ "_id" : ObjectId("6381b45b30aa30888c87e854"), "author" : "mojong", "changeId" : "create-title-index", "executionId" : "2022-11-26T15:38:19.519-989", "changeLogClass" : "com.example.practice.mongock.migration.CreatePostIndex", "changeSetMethod" : "createTitleIndex", "errorTrace" : null, "executionHostname" : "johnnyui-MacBookPro.local", "executionMillis" : NumberLong(40), "metadata" : null, "state" : "EXECUTED", "timestamp" : ISODate("2022-11-26T06:38:19.849Z"), "type" : "EXECUTION" }

flyway의 schema_version 테이블과 같은 역할을 하는 게 mongockChangeLog 인 것 같습니다. 

 

마무리

최근 프로젝트에서 MongoDB를 사용해야 할 일이 있었는데요. 프로젝트를 릴리즈 후 제가 직접 Mongodb에 접속하여 컬렉션을 수동으로 만들고 인덱스도 수동으로 생성하고 이런 작업을 했던 게 생각나서 오늘 한번 Mongock를 사용해봤습니다.  잘만 사용한다면 flyway처럼 편하게 사용할 수 있을 것 같다는 생각이 듭니다. 이런 작은 불편들을 곱씹어(?) 보다 보니 이런 툴들도 찾아 써보게 되네요 ㅎㅎ..

아무튼 여기까지 읽어주셔서 감사합니다. 좋은 하루 보내세요.

반응형