Pink Spider/QueryDSL을 사용할 때 데이터베이스의 Lock

Created Wed, 26 Mar 2025 11:30:45 +0900 Modified Mon, 08 Dec 2025 08:41:47 +0900
1306 Words 6 min

QueryDSL을 사용할 때 데이터베이스의 Lock 기능을 활용하는 방법은 JPA와 유사합니다. QueryDSL은 JPA와 통합되어 동작하며, JPQL 및 Native Query에서 사용하는 잠금 메커니즘을 제공합니다. 아래는 QueryDSL에서 Lock과 관련된 주요 기능과 구현 방법을 설명합니다.


1. QueryDSL에서 Pessimistic Lock 사용

QueryDSL은 JPA LockModeType을 지원하여 비관적 잠금을 구현할 수 있습니다.

예제: LockModeType.PESSIMISTIC_WRITE 사용

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ProductService {
    private final JPAQueryFactory queryFactory;

    public ProductService(EntityManager entityManager) {
        this.queryFactory = new JPAQueryFactory(entityManager);
    }

    @Transactional
    public Product findByIdWithLock(Long productId) {
        QProduct product = QProduct.product;

        // Pessimistic Write Lock 설정
        return queryFactory.selectFrom(product)
                .where(product.id.eq(productId))
                .setLockMode(LockModeType.PESSIMISTIC_WRITE)
                .fetchOne();
    }
}

주요 포인트:

  • setLockMode(LockModeType.PESSIMISTIC_WRITE)를 사용하여 비관적 쓰기 잠금을 설정합니다.
  • PESSIMISTIC_READ와 같은 다른 잠금 모드도 사용할 수 있습니다.

2. 낙관적 잠금 (Optimistic Lock)

낙관적 잠금은 QueryDSL에서 별도로 지원되지 않지만, JPA 엔터티의 @Version 필드를 활용하여 동작합니다.

예제: @Version과 QueryDSL

import jakarta.persistence.EntityManager;
import jakarta.persistence.OptimisticLockException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ProductService {
    private final JPAQueryFactory queryFactory;

    public ProductService(EntityManager entityManager) {
        this.queryFactory = new JPAQueryFactory(entityManager);
    }

    @Transactional
    public void updateProduct(Long productId, String newName) {
        QProduct product = QProduct.product;

        Product existingProduct = queryFactory.selectFrom(product)
                .where(product.id.eq(productId))
                .fetchOne();

        if (existingProduct == null) {
            throw new RuntimeException("Product not found");
        }

        existingProduct.setName(newName);

        try {
            // @Version 필드로 버전 충돌 검사
            queryFactory.getEntityManager().merge(existingProduct);
        } catch (OptimisticLockException e) {
            throw new RuntimeException("Optimistic lock exception occurred", e);
        }
    }
}

주요 포인트:

  • QueryDSL 자체는 낙관적 잠금을 설정하는 API를 제공하지 않지만, JPA의 @Version 필드를 통해 충돌을 관리할 수 있습니다.
  • 충돌 발생 시 OptimisticLockException이 던져집니다.

3. Native Query와 QueryDSL

QueryDSL은 JPQL과 Native Query를 모두 지원하므로, 잠금이 필요한 경우 Native Query를 사용할 수도 있습니다.

예제: Native Query를 통한 Lock 설정

import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ProductService {
    private final EntityManager entityManager;

    public ProductService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Transactional
    public void lockTableWithNativeQuery() {
        Query lockQuery = entityManager.createNativeQuery("LOCK TABLES product WRITE");
        lockQuery.executeUpdate();

        // 데이터 작업 수행

        Query unlockQuery = entityManager.createNativeQuery("UNLOCK TABLES");
        unlockQuery.executeUpdate();
    }
}

주요 포인트:

  • QueryDSL이 제공하지 않는 잠금 동작은 Native Query로 구현할 수 있습니다.
  • LOCK TABLES 또는 SELECT ... FOR UPDATE를 활용하여 데이터 잠금을 설정합니다.

4. QueryDSL + 트랜잭션 격리 수준

Spring의 트랜잭션 격리 수준을 활용하여 QueryDSL에서도 잠금 동작을 제어할 수 있습니다.

예제: 트랜잭션 격리 수준 설정

import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ProductService {
    private final JPAQueryFactory queryFactory;

    public ProductService(EntityManager entityManager) {
        this.queryFactory = new JPAQueryFactory(entityManager);
    }

    @Transactional(isolation = Isolation.SERIALIZABLE)
    public Product findAndUpdate(Long productId, String newName) {
        QProduct product = QProduct.product;

        Product existingProduct = queryFactory.selectFrom(product)
                .where(product.id.eq(productId))
                .fetchOne();

        if (existingProduct == null) {
            throw new RuntimeException("Product not found");
        }

        existingProduct.setName(newName);

        // 저장
        queryFactory.getEntityManager().merge(existingProduct);

        return existingProduct;
    }
}

주요 포인트:

  • @Transactional(isolation = Isolation.SERIALIZABLE)를 설정하여 가장 높은 수준의 격리를 구현합니다.
  • QueryDSL로 데이터를 조회하고 변경할 때 잠금을 유지합니다.

5. Pessimistic Lock과 트랜잭션 동시 사용

QueryDSL의 setLockMode는 트랜잭션과 결합하여 데이터베이스 레벨에서 안전한 동작을 보장합니다.

@Transactional
public Product findByIdWithTransactionAndLock(Long productId) {
    QProduct product = QProduct.product;

    // 트랜잭션 내에서 잠금을 설정
    return queryFactory.selectFrom(product)
            .where(product.id.eq(productId))
            .setLockMode(LockModeType.PESSIMISTIC_WRITE)
            .fetchOne();
}

요약

  1. 비관적 잠금 (Pessimistic Lock): setLockMode(LockModeType.PESSIMISTIC_WRITE)를 사용.
  2. 낙관적 잠금 (Optimistic Lock): JPA의 @Version 필드와 결합.
  3. Native Query: QueryDSL과 함께 사용하여 잠금을 명시적으로 제어.
  4. 트랜잭션 격리 수준: Spring의 트랜잭션 관리와 함께 QueryDSL을 사용.

QueryDSL은 JPA와의 깊은 통합을 통해 잠금 기능을 지원하므로, 데이터베이스의 동작을 제어하면서 동시에 코드 가독성을 유지할 수 있습니다.