1. QueryDSL과 Specification의 사용 비교
1. QueryDSL의 장점
- 강력한 타입 안전성
- 컴파일 타임에 쿼리 오류를 감지할 수 있어, 런타임 오류를 줄일 수 있습니다.
- 간결하고 직관적인 코드
- 코드 자동 생성(Q-class)을 통해 IDE 지원을 받을 수 있으며, 복잡한 쿼리 작성도 상대적으로 간단합니다.
- 복잡한 쿼리에 유리
- 조인, 서브쿼리 등 복잡한 쿼리 작성이 용이하며, 직관적으로 처리할 수 있습니다.
- 재사용 가능
- 다양한 조건 조합을 함수화하여 재사용이 용이합니다.
2. Specification의 장점
- Spring Data JPA와의 완벽한 통합
- JpaSpecificationExecutor를 통해 간단히 동적 쿼리를 추가할 수 있으며, 페이징 및 정렬 기능도 쉽게 사용 가능합니다.
- 유연한 동적 조건 처리
- 조건을 모듈화하여 필요에 따라 조합 가능.
- 초기 설정이 간단
- 별도의 코드 생성기 또는 추가적인 의존성 없이 바로 사용 가능합니다.
- 표준 JPA 기반
- Spring JPA의 기본 기능과 동일한 방식으로 작동하므로 학습 곡선이 낮음.
2.어떤 방법이 더 많이 사용되나?
QueryDSL이 선호되는 경우
- 대규모 프로젝트: QueryDSL은 타입 안전성과 복잡한 쿼리 작성에 강력하기 때문에, 대규모 프로젝트에서 선호됩니다.
- 복잡한 데이터 처리: 조인이나 서브쿼리 등 데이터 처리 로직이 복잡한 경우 QueryDSL이 더 적합합니다.
- 엔터프라이즈 환경: 대기업이나 금융권에서는 안정성과 성능을 이유로 QueryDSL을 자주 선택합니다.
Specification이 선호되는 경우
- 간단한 동적 조건 처리: 조건이 많지 않거나 비교적 단순한 경우, Specification은 더 직관적이고 간단하게 적용할 수 있습니다.
- Spring Data JPA 기반 프로젝트: Spring Data JPA와의 통합성이 좋아 학습 부담이 적습니다.
- 소규모 또는 중소규모 프로젝트: 빠르게 동적 조건을 처리하고자 하는 경우 Specification이 더 효율적입니다.
현재 추세
QueryDSL
- 여전히 많은 프로젝트에서 표준으로 사용되고 있으며, 특히 대규모 및 복잡한 데이터 처리를 요구하는 프로젝트에서 강세입니다.
- 하지만 초기 설정(코드 생성기 필요)과 사용법 학습이 필요하기 때문에, 간단한 프로젝트에서는 다소 과할 수 있습니다.
Specification
- Spring Boot와 Spring Data JPA가 기본 도구로 사용되는 프로젝트에서 빠르게 성장하고 있습니다.
- 특히 최근 Spring Boot의 유연성과 간결성을 선호하는 소규모 팀에서 많이 채택합니다.
결론
- 복잡한 쿼리와 대규모 프로젝트: QueryDSL
- 단순한 동적 조건과 소규모 프로젝트: Specification
실제로 어떤 방식이 더 많이 사용되는지는 프로젝트 요구사항과 팀의 선호도에 따라 다릅니다.
QueryDSL은 여전히 많이 사용되지만, 최근에는 Spring Data JPA의 강력한 통합 덕분에
Specification 방식도 점점 더 많이 사용되고 있습니다.
따라서, 두 가지를 모두 학습하고 프로젝트에 적합한 방법을 선택하는 것이 가장 현명합니다.
3.Specification 사용방법
1. Specification의 핵심 개념
- Specification<T> 인터페이스:
-
- 제네릭 타입 T는 엔티티 클래스를 의미합니다.
- 주요 메서드: toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder)
-
- Root<T>: 엔티티의 루트 객체 (SQL의 FROM 절 역할).
- CriteriaQuery<?>: JPA Criteria 쿼리 객체.
- CriteriaBuilder: 동적 쿼리를 생성하는 데 필요한 도구를 제공하는 클래스.
2. 사용 방법
2.1. 기본 설정
- 엔티티 클래스와 Repository를 생성합니다.
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String category;
private Double price;
// Getter, Setter, Constructor
}
Repository 생성
JpaSpecificationExecutor를 확장한 Repository를 생성합니다.
public interface ProductRepository extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {
}
2.2. Specification 구현
조건을 정의하는 Specification을 작성합니다.
Example 1: 단일 조건 (카테고리로 필터링)
public class ProductSpecifications {
public static Specification<Product> hasCategory(String category) {
return (root, query, builder) -> builder.equal(root.get("category"), category);
}
}
Example 2: 가격 범위 필터링
public class ProductSpecifications {
public static Specification<Product> priceBetween(Double minPrice, Double maxPrice) {
return (root, query, builder) -> builder.between(root.get("price"), minPrice, maxPrice);
}
}
2.3. Specification 조합
- Specification은 AND, OR 연산을 사용하여 조합할 수 있습니다.
import org.springframework.data.jpa.domain.Specification;
public class ProductSpecifications {
public static Specification<Product> hasCategory(String category) {
return (root, query, builder) -> builder.equal(root.get("category"), category);
}
public static Specification<Product> priceBetween(Double minPrice, Double maxPrice) {
return (root, query, builder) -> builder.between(root.get("price"), minPrice, maxPrice);
}
public static Specification<Product> nameContains(String keyword) {
return (root, query, builder) -> builder.like(root.get("name"), "%" + keyword + "%");
}
}
조합 예시
Specification<Product> spec = Specification
.where(ProductSpecifications.hasCategory("Electronics"))
.and(ProductSpecifications.priceBetween(100.0, 500.0))
.or(ProductSpecifications.nameContains("Smart"));
2.4. Repository에서 사용
Repository의 메서드에서 Specification을 전달하여 조건에 맞는 데이터를 조회합니다.
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<Product> getFilteredProducts(String category, Double minPrice, Double maxPrice, String keyword) {
Specification<Product> spec = Specification
.where(ProductSpecifications.hasCategory(category))
.and(ProductSpecifications.priceBetween(minPrice, maxPrice))
.or(ProductSpecifications.nameContains(keyword));
return productRepository.findAll(spec);
}
}
3. 장점
1.동적 쿼리 작성: 런타임에 동적으로 조건을 조합할 수 있습니다.
2. 모듈화: 조건을 각각의 Specification으로 분리하여 재사용이 가능합니다.
3. Spring Data와 통합: 페이징 및 정렬과 함께 사용할 수 있습니다.
4. 페이징과 정렬 지원
- Pageable 객체와 함께 사용하여 페이징된 결과를 반환할 수 있습니다.
Page<Product> result = productRepository.findAll(spec, PageRequest.of(0, 10, Sort.by("price").descending()));
5. 실제 사용 예시
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public List<Product> getProducts(
@RequestParam String category,
@RequestParam Double minPrice,
@RequestParam Double maxPrice,
@RequestParam String keyword
) {
return productService.getFilteredProducts(category, minPrice, maxPrice, keyword);
}
}
6. 한계 및 주의점
- 복잡한 쿼리: 매우 복잡한 조합은 코드가 길어질 수 있어, QueryDSL이 더 적합한 경우가 있습니다.
- 초기 학습 곡선: JPA Criteria API와의 친숙함이 필요합니다.
- 성능 주의: 불필요한 조건 조합은 성능 문제를 초래할 수 있습니다.
4.Specification 사용예
1) Novel
import java.util.ArrayList;
import java.util.List;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import com.web.domain.base.BaseTimeEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@NoArgsConstructor
@DynamicInsert
@DynamicUpdate
@Table(name = "novel")
@AllArgsConstructor
@Builder
public class Novel extends BaseTimeEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "novel_id")
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String author;
@Column(nullable = false)
private String category;
@Column(name = "description", nullable = false)
@Lob
private String description; // 소설 설명
@Column(name = "cover_image_url")
private String coverImageUrl; // 소설 표지 이미지 경로
@ColumnDefault("0")
@Column(name = "view_count", nullable = false) // 조회수
private Integer viewCount;
@Column(name = "is_paid", nullable = false) // 무료/유료 여부
private boolean paid;
@ColumnDefault("0")
@Column(name = "like_Score", nullable = false)
private Integer likeScore; //좋아요 싫어요 점수 -추천 비추천 점수
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "users_id")
private User user;
@OneToMany(mappedBy = "novel", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Chapter> chapters = new ArrayList<>();
@OneToMany(mappedBy = "novel", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Like> likes =new ArrayList<Like>();
public void incrementViewCount() {
this.viewCount++;
}
}
2)NovelSpecifications
import org.springframework.data.jpa.domain.Specification;
import com.web.domain.Novel;
public class NovelSpecifications {
public static Specification<Novel> titleContains(String title) {
return (root, query, builder) -> builder.like(root.get("title"), "%" + title + "%");
}
public static Specification<Novel> categoryEquals(String category) {
return (root, query, builder) -> builder.equal(root.get("category"), category);
}
public static Specification<Novel> isPaid(Boolean paid) {
return (root, query, builder) -> builder.equal(root.get("paid"), paid);
}
public static Specification<Novel> authorContains(String author) {
return (root, query, builder) -> builder.like(root.get("author"), "%" + author + "%");
}
public static Specification<Novel> searchQueryContains(String searchQuery) {
return (root, query, builder) -> builder.or(
builder.like(root.get("title"), "%" + searchQuery + "%"),
builder.like(root.get("author"), "%" + searchQuery + "%"),
builder.like(root.get("category"), "%" + searchQuery + "%")
);
}
}
3)Controller
@GetMapping
public String listNovels(NovelSearchDTO searchDTO, PageMaker pageMaker,Model model) {
Page<Novel> items =novelService.listNovels(searchDTO, pageMaker.getPageable(pageMaker, 12));
String pagination = pageMaker.pageObject(items, pageMaker.currentPage(pageMaker), 12, 5, "/novels", "href");
List<Novel> content = items.getContent();
model.addAttribute("novels",content);
model.addAttribute("searchDTO", searchDTO);
model.addAttribute("pagination", pagination);
return "novels/list";
}
4)NovelServiceImpl
/**
* 검색처리
*/
@Override
public Page<Novel> listNovels(NovelSearchDTO searchDTO, PageRequest pageRequest) {
Specification<Novel> spec = Specification.where(null);
// 검색 조건 추가
if (searchDTO.getSearchType() != null) {
switch (searchDTO.getSearchType()) {
case "title":
spec = spec.and(NovelSpecifications.titleContains(searchDTO.getSearchQuery()));
break;
case "author":
spec = spec.and(NovelSpecifications.authorContains(searchDTO.getSearchQuery()));
break;
case "category":
spec = spec.and(NovelSpecifications.categoryEquals(searchDTO.getSearchQuery()));
break;
case "all":
default:
spec = spec.and(NovelSpecifications.searchQueryContains(searchDTO.getSearchQuery()));
break;
}
}
// 정렬 조건 추가
Sort sort = Sort.by(Sort.Direction.DESC, "createDate"); // 기본값: 등록일순
if ("likeScoreDesc".equals(searchDTO.getOrderBy())) {
sort = Sort.by(Sort.Direction.DESC, "likeScore");
} else if ("viewCountDesc".equals(searchDTO.getOrderBy())) {
sort = Sort.by(Sort.Direction.DESC, "viewCount");
}
Pageable sortedPageRequest = PageRequest.of(
pageRequest.getPageNumber(),
pageRequest.getPageSize(),
sort
);
return novelRepository.findAll(spec, sortedPageRequest);
}

















댓글 ( 0)
댓글 남기기