들어가며
JPA를 선택하고 나서 바로 다음 고민이 시작됐습니다.
StyleHub는 상품 검색처럼 조건이 여러 개 조합되는 쿼리가 많은 도메인입니다. JPA만으로 이 문제를 해결할 수 있는지 설계 단계에서 미리 검토했고, 그 과정에서 QueryDSL 도입을 결정했습니다.
1. 설계 단계에서 마주한 질문
JPA를 선택한 뒤 상품 검색 기능을 설계하면서 스스로에게 질문을 던졌습니다.
"카테고리, 가격 범위, 스토어를 동시에 필터링하는 쿼리를 JPA로 어떻게 구현할 것인가?"
JPQL로 구현하면 어떻게 될지 먼저 그려봤습니다.
String jpql = "SELECT p FROM Product p WHERE 1=1";
if (categoryId != null) jpql += " AND p.category.id = :categoryId";
if (minPrice != null) jpql += " AND p.price >= :minPrice";
if (storeId != null) jpql += " AND p.store.id = :storeId";
두 가지 문제가 보였습니다.
첫 번째, 오타가 런타임에서야 발견됩니다. 컬럼명을 문자열로 직접 작성하기 때문에 오타가 있어도 컴파일 타임에 잡히지 않습니다. 결제처럼 민감한 도메인에서 런타임 오류는 치명적입니다.
두 번째, 조건이 늘어날수록 코드가 뒤엉킵니다. StyleHub의 상품 검색 필터는 카테고리, 가격, 스토어 외에도 더 늘어날 수 있습니다. 문자열 조합 방식은 조건이 추가될수록 유지보수가 어려워지는 구조입니다.
2. Criteria API는 왜 선택하지 않았는가
JPA가 제공하는 Criteria API는 타입 안정성을 지원합니다. 그런데 같은 쿼리를 Criteria API로 작성하면 이렇게 됩니다.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Product> query = cb.createQuery(Product.class);
Root<Product> p = query.from(Product.class);
List<Predicate> predicates = new ArrayList<>();
if (categoryId != null)
predicates.add(cb.equal(p.get("category").get("id"), categoryId));
if (minPrice != null)
predicates.add(cb.greaterThanOrEqualTo(p.get("price"), minPrice));
query.where(predicates.toArray(new Predicate[0]));
타입 안정성은 확보됐지만 코드가 장황하고 읽기 어렵습니다. 실제로 이 코드를 팀원이 나중에 읽었을 때 어떤 쿼리인지 한눈에 파악하기 어렵다고 판단했습니다. 유지보수 비용이 JPQL보다 오히려 높아지는 구조였습니다.
3. QueryDSL을 선택한 이유
QueryDSL은 JPQL의 타입 안정성 문제와 Criteria API의 복잡함을 동시에 해결했습니다.
QProduct p = QProduct.product;
BooleanBuilder builder = new BooleanBuilder();
if (categoryId != null) builder.and(p.category.id.eq(categoryId));
if (minPrice != null) builder.and(p.price.goe(minPrice));
if (storeId != null) builder.and(p.store.id.eq(storeId));
queryFactory.selectFrom(p).where(builder).fetch();
컬럼명을 코드로 참조하기 때문에 오타가 생기면 컴파일 타임에 즉시 오류가 발생합니다. 코드 구조도 SQL과 비슷해서 읽기 편하고, 조건이 늘어나도 builder.and()로 자연스럽게 확장할 수 있습니다.
4. 단순 조회는 Spring Data JPA, 동적 쿼리는 QueryDSL
QueryDSL을 모든 쿼리에 사용하는 건 오히려 비효율적입니다. 단순 단건 조회에 QueryDSL을 사용하면 코드가 불필요하게 복잡해집니다.
그래서 저희는 역할을 나눴습니다.
- Spring Data JPA: findById(), findAll() 같은 단순 조회
- QueryDSL: 조건이 동적으로 변하는 복잡한 조회
두 기술을 상황에 맞게 함께 활용하는 방향으로 설계했습니다.
마치며
QueryDSL 도입은 단순히 "많이 쓰니까"가 아니었습니다.
JPQL의 문자열 조합 방식이 가져오는 런타임 오류 위험과 유지보수 비용을 설계 단계에서 미리 인지했고, Criteria API도 검토했지만 복잡함 때문에 제외했습니다. 선택한 기술의 한계를 인지하고 보완할 수 있을 때 비로소 그 기술을 제대로 쓰고 있다고 생각합니다.
'stylehub 프로젝트' 카테고리의 다른 글
| [StyleHub#7]Transactional이 동작하지 않는다? — Spring Self-Invocation 버그 발견과 해결 (0) | 2026.03.17 |
|---|---|
| [StyleHub#6]@Transactional 범위 최소화로 커넥션 점유 시간을 줄인 과정 (feat. BCrypt) (1) | 2026.03.15 |
| [StyleHub #3]Spring이라서 JPA를 선택하지 않았습니다 — StyleHub에서 JDBC, MyBatis, JPA를 비교한 이유 (0) | 2026.03.09 |
| [StyleHub #2]커머스 DB 설계: ERD 설계하면서 가장 많이 고민했던 9가지 (0) | 2026.03.08 |
| [StyleHub #1]프로젝트 주제 선정 이유 - feat : 무신사 (1) | 2026.03.04 |