MongoDB 동시성 제어
개요
RDB에서는 트랜잭션의 격리 수준에 따라서 동시성 제어가 필요했습니다.
Lock을 거는방식 혹은 Versoning을 통하여 동시성을 제어했습니다.
MongoDB에서는 어떻게 동시성 제어가 일어날까요?
Atomicity and Transactions
mongoDB의 공식문서인 Atomicity and Transactions 파트를 읽어보았습니다.
MongoDB에서는 단일 문서 내에 포함된 여러 문서를 수정하는 경우에도 원자적 연산이 지원된다고 합니다.
다만 updateMany처럼 여러 문서를 수정하는 경우에 각 문서의 수정은 원자적이지만 전체 작업은 원자적이지 않습니다.
여러 문서를 쓰기 작업을 할 때 다른 작업이 끼어들 수 있습니다.
Concurrency Control
여러 애플리케이션에서 동시에 실행하기 위해서는 데이터 불일치나 충돌이 발생하지 않도록 동시성 제어를 해주어야 합니다.
db.myCollection.findAndModify( {
query: { a: 1 },
update: { $inc: { b: 1 }, $set: { a: 2 } }
} )
문서에 대한 findAndModify 명령어는 하나의 문서에 대해 원자적입니다.
query 조건에 일치하면 update를 수행하며 갱신되기 전까지는 해당 문서에 대한 query 및 update는 수행하지 않습니다.
MongoDB Read and Update로 데이터 경합시켜 보기
@Service
class BoardService(
private val boardRepository: BoardRepository,
){
fun findPostBy(id: String): Board {
return boardRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("${id}로 게시글을 조회할 수 없습니다")
}
@Transactional
fun registerPost(): String{
return boardRepository.save(Board()).id!!
}
@Transactional
fun increaseViewCount(id: String): Board{
val board = boardRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("${id}로 게시글을 조회할 수 없습니다")
logger.info { "조회수 증가가 호출되었습니다"}
board.addHits()
return boardRepository.save(board)
}
@Transactional
fun deletePost(id: String){
boardRepository.deleteById(id)
}
}
Spring을 활용하여 MongoDB로 게시글을 등록, 조회, 조회수증가 api를 만들었습니다.
조회수증가에서는 id로 게시글을 조회하고 해당 id의 조회수를 1 증가시키는 로직을 만들었습니다.
read and update를 구현하고 RDB를 활용하는 경우에는 데이터 정합성이 깨지게 됩니다.
많은 스레드가 조회수가 0인 상태의 게시글을 읽고 이후에 조회수 1을 더해서 저장하기 때문에 모든 호출이 반영되지 않습니다.
성능테스트를 통해 60번을 호출해 보면 mongoDB에서는 결과가 어떻게 될까요?
RDB와 동일하게 데이터 정합성이 깨지게 됩니다.
로그를 살펴보면 60번 호출되었지만 게시글의 조회수는 60이 증가하지 않았습니다.
mongoDB를 활용하더라도 read and update를 수행하면 데이터 정합성은 보장되지 않습니다.
MongoTemplate findAndModify 활용
@Service
class BoardService(
private val boardRepository: BoardRepository,
private val mongoTemplate: MongoTemplate,
){
...
@Transactional
fun increaseViewCountV2(id: String): Board{
val query = Query(Criteria.where("_id").`is`(id))
val update = Update().inc("hit", 1)
val options = FindAndModifyOptions().returnNew(true)
return mongoTemplate.findAndModify(query, update, options, Board::class.java)
?: throw IllegalArgumentException("${id}로 게시글을 조회할 수 없습니다")
}
}
mongoTemplate을 활용하여 findAndModify 명령어를 사용하였습니다.
이후에 성능테스트를 통해 60번 호출해보면 조회수가 60이 증가하면서 데이터 정합성이 보장됩니다.
즉, MongoDB에서 원자성이 지원되는 연산인지 잘 확인하면서 수행해야 합니다.
마무리
mongoDB를 사용하더라도 동시성 제어에 주의해야 합니다.
즉, 사용하는 연산이 원자적인지 확인해야 합니다.
다음글에서는 동시성제어와 더불어 비관적 Lock, 낙관적 Lock, mongoTemplate의 역할, MongoRepository에 @Query와 같은 형식으로 구현하는 방법들을 찾아보고자 합니다.
참고자료
https://www.mongodb.com/docs/manual/core/write-operations-atomicity/