Check-Then-Act 패턴에서 발생한 동시성 문제 해결 과정

이준혁·2024년 6월 23일
0

테이블링 애플리케이션을 개발하면서 동시성 문제가 발생했고, 이 때문에 비즈니스 수준의 정합성을 지킬 수 없게 되었습니다. 이 글에서는 두 가지 해결 방법과 주의 사항을 다루고 있습니다.

식당 대기 시스템

사용자가 식당에 대기 요청을 보내면 대기 가능 여부를 확인한 후, 요청이 수락됩니다.

만약 식당이 더 이상 대기열을 수용할 수 없다면, 요청은 거절됩니다.

문제 상황

대기 요청은 Check-Then-Act 패턴으로 동작하며, 현재 '대기 가능 인원수'를 확인한 후 요청을 수락합니다.

하지만 다수의 사용자가 동시에 접근하면, Check 단계에서 조회한 '대기 가능 인원수'가 유효성을 잃어 식당의 최대 대기 가능 인원수를 초과하게 됩니다.

Exclusive Lock

'대기 가능 인원수'가 유효성을 잃지 않으려면, Check 단계와 Act 단계를 원자적으로 처리해야 합니다. 우선 Exclusive Lock을 사용해 문제 해결을 시도했습니다.

먼저 진입한 트랜잭션이 Lock을 획득하면, 해당 트랜잭션이 종료될 때까지 다음 트랜잭션은 대기하게 됩니다. 이를 통해 동시성 문제를 해결하고, 비즈니스 수준의 정합성을 지킬 수 있습니다.

Lock이 적용되지 않는 문제

JPA를 사용한다면 주의 사항이 있습니다. 선언적으로 Pessimistic Lock을 사용할 때, 아래 로그와 함께 실제 DB에 Lock이 적용되지 않는 문제가 발생할 수 있습니다.

org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess - Encountered request for locking however dialect reports that database prefers locking be done in a separate select (follow-on locking); results will be locked after initial query executes

Follow-on lock

DeferredResultSetAccess 클래스를 확인해 본 결과, JPA가 내부적으로 Exclusive Lock을 Follow-on Lock으로 변환하는 것을 확인했습니다.

Follow-on Lock이란?

  • 즉시 락을 적용하지 않고, 쿼리 실행 후 별도의 단계에서 락을 적용하는 메커니즘
  • DB 종류에 따라 특정 쿼리에서 Lock을 지원하지 않음
    • 이때, DB에 Lock이 적용되지 않도록 JPA 내부적으로 최적화를 수행

간혹 이와 관계없이 최적화가 수행되기도 합니다. 이런 경우, 먼저 실행하고자 하는 쿼리가 현재 DB 환경에서 정상적으로 동작하는지 확인해야 합니다. 그 후, Native Query를 사용하거나 LockOptions 클래스의 설정 변경을 통해 정상적으로 Lock을 적용할 수 있습니다.

Exclusive Lock의 문제점

동시성 문제는 해결했지만, 모든 대기 요청에 Lock이 적용되기 때문에 동시성이 떨어지게 됩니다.

모든 요청이 원자적으로 처리될 필요는 없기 때문에 동시성을 높일 방법에 대해 고민해 보았습니다.

Named Lock

식당 id로 key를 만들어 Named Lock을 사용했고, 이를 통해 Lock 적용 범위를 식당 단위로 축소할 수 있었습니다.

트랜잭션 전파 수준

Exclusive Lock과 달리 Named Lock은 직접 해제해야 합니다. JPA를 사용할 경우, 기본적으로 트랜잭션이 종료될 때까지 실제 쿼리가 DB에 커밋되지 않습니다. 이 때문에 실행 쿼리와 Named Lock의 해제가 동일한 트랜잭션에서 수행된다면 동시성 문제를 해결할 수 없습니다. 따라서 새로운 트랜잭션을 만들어 DB에 커밋한 후, Named Lock을 해제해야 합니다.

Named Lock의 문제점

식당 단위로 Lock을 적용해 동시성 문제를 해결하고 비즈니스 수준의 정합성을 지켰습니다. 하지만 아직 개선이 필요한 부분이 있습니다.

커넥션 부족 문제

Named Lock을 사용하는 과정에서 커넥션이 추가로 사용됩니다. 단일 데이터베이스 환경이기 때문에 커넥션 부족 현상이 발생할 수 있습니다.

Check-Then-Act 방식의 한계

Check과 Act 단계가 원자적으로 처리되어야 하므로 수용 인원에 여유가 있더라도 매번 Lock을 적용해야 합니다. 만약 특정 식당에 요청이 몰리게 될 경우, 처리 중인 요청을 제외한 나머지 요청은 대기하게 되고, 이 시간 동안 스레드 자원을 점유하게 됩니다. 이는 애플리케이션 전체의 처리 속도에 영향을 미칠 수 있습니다.

마무리

동시성 문제로 인해 비즈니스 수준의 정합성을 지킬 수 없었지만, Named Lock을 활용해 이를 해결했습니다. 여전히 개선이 필요한 부분이 있으며, 추후 Redis를 활용하거나 직접 큐를 구현해서 해결해 보려고 합니다. 간단하게 정리하며 글을 마칩니다.

  • 식당 대기 시스템은 Check-Then-Act 패턴으로 동작하기 때문에 비즈니스 수준의 정합성을 지키려면 Check과 Act 단계가 원자적으로 처리되어야 합니다.
  • Exclusive Lock을 사용하면 모든 요청에 Lock이 적용되기 때문에 동시성이 떨어집니다.
  • Named Lock을 사용하면 동시성을 높일 수 있지만, 커넥션 부족 현상이 발생할 수 있으며 여전히 Check-Then-Act 방식의 한계를 극복하지 못합니다.

참고 자료

https://github.com/octachrome/innodb-locks
https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/chapters/locking/Locking.html
https://mangkyu.tistory.com/299

profile
조금이라도 매일 실천하는 것을 좋아합니다.

0개의 댓글