모종닷컴

MySQL vs MongoDB Atomic Counter 비교 본문

Programming

MySQL vs MongoDB Atomic Counter 비교

모종 2022. 9. 9. 16:54
반응형

Counter



atomic counter는 원자성 카운터입니다. 좀 더 쉽게 풀어 설명하자면 동기화된 카운터라고 이해할 수 있을 것 같아요. 카운터는 숫자를 1, 2, 3.. 이런 식으로 숫자를 세는 프로그램입니다. 애플리케이션 내에서는 이 카운터를 다양한 방법으로 동기화시켜 만들 수 있지만 전체 시스템이라면 얘기가 달라집니다.

시스템에서 카운터를 만드는 방법은 여러 가지 있지만 저는 이걸 데이터베이스를 사용하여 구현하고자 합니다. MySQL의 auto-increment를 이용한 카운터를 하는 방법과 mongodb의 findAndModify를 사용해서 구현하였고 이를 비교해보았습니다.

코드도 붙여넣을 예정인데 구현은 Spring+Kotlin으로 하였고 추가적으로 spring-boot-data-jpa, spring-boot-data-mongodb 를 이용하였습니다.

MySQL 원자성 카운터 구현하기

MySQL에서는 auto increment가 원자성 카운터와 동일하게 동작합니다. 정확한 설명은 다루지 않겠지만 auto increment를 하기 전 lock을 걸게 됩니다. 

테이블 생성

CREATE TABLE `seq_generator` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

 

엔티티

@Entity(name = "seq_generator")
class SeqGenerator(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Int = 0
)
@Repository
interface SeqGeneratorRepository: JpaRepository<SeqGenerator, Int>

카운터 코드

@Service
class SeqGeneratorService(
    private val seqGeneratorRepository: SeqGeneratorRepository
) {
    fun nextVal(): Int = seqGeneratorRepository.save(SeqGenerator()).id
}

카운터 성능 측정

private val log = KotlinLogging.logger {  }

@RestController
@RequestMapping("/api/seq")
class SeqTestController(
    private val seqGeneratorService: SeqGeneratorService
) {
    @PostMapping("/mysql")
    fun generateSeqWithRDB(@RequestParam size: Int): ResponseEntity<String> {
        val start = System.currentTimeMillis()

        repeat(size) { seqGeneratorService.nextVal() }

        val end = System.currentTimeMillis()
        log.info { "start = $start, end = $end" }

        return ResponseEntity.ok("${((end - start) / 1000.0)}초")
    }
}

호출하기 위해 테스트 코드를 만들어되지만 스프링 부트 테스트 상에 어떤 설정이 있을지 모르니 그냥 컨트롤러를 작성해서 호출하도록 하겠습니다. size는 1000개로 추가해서 위 엔드포인트를 실행시키면 저는 평균적으로 1.6초라는 결과를 얻을 수 있었습니다. 또한 테이블의 auto increment를 보면 카운터도 잘 동작했구요.

멀티 스레드로 테스트

@Service
class AsyncService {
    private val executor = Executors.newFixedThreadPool(10)

    fun <T> run(action: () -> T): T {
        val futures = CompletableFuture.supplyAsync(action, executor)
        return futures.get()
    }
}

 

위에서는 스레드 하나로만 돌렸을테니 멀티 스레드 환경에서도 잘 동작하는지 확인을 해보겠습니다. 일단 간단한 비동기 서비스를 하나 작성하였습니다. 스프링 부트를 사용한다면 기본적으로 hikari maximum pool size가 10으로 되어있는 것 같아서 풀 사이즈는 10개만 지정하였습니다. 비동기 서비스가 추가되었다면 컨트롤러에 repeat에 해당하는 부분을 아래처럼 수정하겠습니다.

repeat(size) {
    asyncService.run { seqGeneratorService.nextVal() }
}

다시 실행을 해보면 저는 평균적으로 1.0 ~ 1.1초 대의 결과를 얻을 수 있었습니다. 멀티 스레드 환경에서는 좀 더 성능이 올라가는군요. 또한 멀티 스레드 환경에서도 테이블의 원자성 카운터는 제대로 동작을 하고 있습니다. 

 

MongoDB로 원자성 카운터 구현하기

Document

@Document(collection = "seq_generator")
class CollectionSeqGenerator(
    @Id
    var id: Int,
    var seq: Int
)

카운터 코드

@Service
class CollectionSeqGeneratorService(
    private val mongoTemplate: MongoTemplate
) {
    fun nextVal(): Int {
        val query = Query()
        query.addCriteria(Criteria.where("_id").`is`(0))
        val update = Update().inc("seq", 1)
        val options = FindAndModifyOptions.options().returnNew(true)
        val seqGenerator = mongoTemplate.findAndModify(
            query,
            update,
            options,
            CollectionSeqGenerator::class.java
        )
        return seqGenerator.seq
    }
}

MongoDB는 딱히 스키마가 없기 때문에 collection과 초기 값만 추가해주었습니다. test 컬렉션을 만들고 seq_generator 도큐먼트를 생성 후 초기값을 아래처럼 넣었습니다.

CP 설정

MySQL과 최대한 동일한 환경을 맞추기 위해서 커넥션풀 설정을 하였습니다. 따라서 mongodb url을 다음과 같이 작성하였습니다.

mongodb://localhost:27017/test?minPoolSize=10&maxPoolSize=10

카운터 성능 측정

위에서 만들었던 SeqTestController에 아래 코드를 추가해주었습니다.

@PostMapping("/mongodb")
fun generateSeqWithMongoDB(@RequestParam size: Int): ResponseEntity<String> {
    val start = System.currentTimeMillis()

    repeat(size) {
        asyncService.run { collectionSeqGeneratorService.nextVal() }
    }

    val end = System.currentTimeMillis()
    log.info { "start = $start, end = $end" }

    return ResponseEntity.ok("${((end - start) / 1000.0)}초")
}

마찬가지로 size는 1000으로 지정해서 호출해보았습니다. 그 결과 평균 0.4 ~0.5초 대의 결과를 얻을 수 있었습니다. MongoDB에 접속하여 카운터도 제대로 동작하고 있음을 확인하였습니다.

테스트 결과

Atomic Counter를 MySQL과 MongoDB를 이용하여 구현해보고 성능을 측정해봤는데 지금 결과를 보아서는 mongodb가 mysql 보다 50% 정도 성능이 더 우세해 보입니다.

다만 단점 또한 보이기도 하는데요. MySQL 원자성 카운터를 사용하기 위해서는 애플리케이션에서 insert만 하면 됩니다. 하지만 MongoDB 원자성 카운터를 이용하기 위해서는 반드시 카운터 코드(findAndModify)가 필요했습니다. 만약 다른 서버에서 mongodb의 원자성 카운터를 잘못 사용하는 부분이 있다면 문제가 발생할 수도 있습니다.

이런 단점을 최대한 커버해보기 위해 저는 공통 모듈을 만들어 원자성 카운터 코드를 구현하였고 다른 모듈에서는 이 코드를 그대로 사용하도록 구성하였습니다.

여기까지 읽어주셔서 감사합니다. 그럼 다들 즐거운 명절되세요~

반응형