jOOQ 동시성(Concurrency) 제어 - Optimistic Lock / Pessimistic Lock

Pir·2022년 1월 23일
post-thumbnail

1. 동시성 제어(Concurrency Control)

❓ 왜 필요한가

  • 스프링 프레임워크는 기본적으로 멀티 쓰레드 환경이기 때문에 각기 다른 요청들에 의해 개발자가 원하지 않는 결과를 얻을 수 있다.

💡 해결 방안

  • 어플리케이션 사이드에서 동시성(쓰레드 기반)을 제어할 수도 있지만 이번 게시글에서는 Java 네이티브 쿼리빌더인 jOOQ를 활용하여 디비 사이드에서 동시성(트랜잭션 기반)을 제어하려고 합니다.

2. 동시성 제어 전 문제점

🔱 50개 쓰레드로 1개의 글 1번씩 조회

  • 락 걸지 않고 테스트
    public class NoLockWorker implements Runnable{
        private CountDownLatch countDownLatch;

        public NoLockWorker(CountDownLatch countDownLatch){
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            postService.plusHitById(BASE_ID);
            countDownLatch.countDown();
        }
    }
    
    ------------------------------------------------------------------

    public void plusHitById(Long id){
        context.transaction(configuration ->{
            final Post post = DSL.using(configuration)
                                .selectFrom(POST)
                                .where(POST.ID.eq(id))
                                .fetchOneInto(Post.class);

            final Long hit = post.getHit();

            DSL.using(configuration)
                    .update(POST)
                    .set(POST.HIT, hit + 1L)
                    .where(POST.ID.eq(id))
                    .execute();
        });
    }
  • 결과

    ❌ 테스트 실패
    => hit가 50개가 되는 것을 예상했지만, 50개의 멀티 쓰레드의 여러 트랜잭션이 동시에 한 ROW에 몰렸기 때문에 개발자가 원하지 않는 hit가 5라는 결과 도출

3. Optimistmic Lock / Pessimistic Lock

🔒 Optimistic Lock이란?

  • 트랜잭션이 충돌을 하지 않는다고 가정하고 가정합니다. (낙관적 접근)
  • version, timestamp 등 컬럼을 두어 해당 version에서 다른 트랜잭션이 업데이트했을 경우 예외 발생 (업데이트가 있을 경우 버전이 1씩 증가)
  • 업데이트 시점만에 락을 걸기 때문에 실시간 데이터를 보여줘야하는 비즈니스에 유리

🔏 Pessimistic Lock이란?

  • 트랜잭션이 충돌이 많이 발생한다고 가정하고 먼저 락을 겁니다. (비관적 접근)
  • 데이터베이스에서 제공하는 락을 사용합니다. Pessimistic Lock은 많은 종류가 있기 때문에 각 비즈니스마다 선택해서 사용 권장
  • Optimistic Lock과 비교하면 더 안정적인 방식이지만 실시간 데이터를 보여주는 비즈니스의 동시성과는 어울리지 않는다.

4. Optimistic Lock으로 동시성 제어

⭐ Version 컬럼 추가

  • Database (Post 테이블)

  • Gradle
    => jOOQ 설정 시 Gradle에서 사용할 version 컬럼을 지정할 수 있습니다.

DslContext 세팅시 withExecuteWithOptimisticLocking(true) 옵션을 추가한다.
또한, OptimisticLock이 충돌 때문에 예외를 던지면 커넥션을 계속 물고 있는 상황이 생기는데, try with resources를 사용하여 충돌 시 바로 커넥션을 종료시킨다.

    public void plusHitOptimisticById(Long id){
        try(Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD)){
            DSL.using(connection, SQLDialect.MYSQL, new Settings().withExecuteWithOptimisticLocking(true))
                    .transaction(configuration -> {
                        PostRecord postRecord = DSL.using(configuration)
                                                  .fetchOne(POST, POST.ID.eq(id));

                        postRecord.setHit(postRecord.getHit() + 1L);
                        postRecord.store();
                    });
        } catch (SQLException e) {
            log.info("error code : {}, error : {}", e.getErrorCode(), e);
        }
    }
  • 결과

    ✅ 테스트 통과
    => 같은 버전에 2개 이상의 트랜잭션이 충돌하면 선행 트랜잭션의 업데이트만 성공하고 나머지 트랜잭션은 예외를 발생시킵니다. 해당 예외를 잡고 리트라이해서 동시성을 제어합니다.

5. Pessimistic Lock으로 동시성 제어

⭐ forUpdate를 활용한 Pessimistic Lock

forUpdate는 다른 트랜잭션의 CRUD 모두 허용하지 않는 특징이 있다.

  • DslContext 트랜잭션의 해당 데이터 조회 부분에 forUpdate라는 메서드를 추가한다.
  • 결과

    ✅ 테스트 통과
    => 한 쓰레드 당 하나의 트랜잭션을 가지고 ROW에 접근하여 순서대로 조회 -> 업데이트하여 hit가 50이라는 결과를 도출할 수 있다.

느낀 점

💡

앞으로 동시성을 제어해야하 하는 상황에서, 어플리케이션 레벨에서 제어를 할지 데이터베이스 레벨에서 제어를 할지 조금은 더 고민해볼 수 있는 시간이 될 것 같습니다.
데이터베이스 레벨에서 제어한다고 하면 어떤 Lock이 더 비즈니스에 맞을 지 생각하고 선택할 수 있고, Optimistic Lock을 사용하면 어떤 식으로 구성해야할지 고민해볼 것 같습니다.
이번에 개인적으로 공부하면서 저희 파트에도 공유하고 회사 프로젝트에 적용했던 기회였기 때문에 더 재밌게 진행할 수 있었던 것 같습니다.

출처

https://www.jooq.org/doc/latest/manual/sql-execution/crud-with-updatablerecords/optimistic-locking/

profile
흉내내는 사람이 아닌, 이해하는 사람이 되자

2개의 댓글

comment-user-thumbnail
2022년 1월 23일

좋은 글 감사합니다. jooq 관련해서 조금 더 써주시면 감사할 것 같아요!

답글 달기
comment-user-thumbnail
2022년 2월 1일

글 잘 읽었습니다ㅎㅎ 전체 실행속도 또한 비관적 락보다 빠르게 나오나요?

db에 쓰기 쓰레드는 하나만 접근 할 수 있고 나머지는 다 버려지지는 않을지 궁금하네요!

답글 달기