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();
}
요약
- 비관적 잠금 (Pessimistic Lock):
setLockMode(LockModeType.PESSIMISTIC_WRITE)를 사용. - 낙관적 잠금 (Optimistic Lock): JPA의
@Version필드와 결합. - Native Query: QueryDSL과 함께 사용하여 잠금을 명시적으로 제어.
- 트랜잭션 격리 수준: Spring의 트랜잭션 관리와 함께 QueryDSL을 사용.
QueryDSL은 JPA와의 깊은 통합을 통해 잠금 기능을 지원하므로, 데이터베이스의 동작을 제어하면서 동시에 코드 가독성을 유지할 수 있습니다.