스프링

 

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이 더 효율적입니다.

 

현재 추세

  1. QueryDSL

    • 여전히 많은 프로젝트에서 표준으로 사용되고 있으며, 특히 대규모 및 복잡한 데이터 처리를 요구하는 프로젝트에서 강세입니다.
    • 하지만 초기 설정(코드 생성기 필요)과 사용법 학습이 필요하기 때문에, 간단한 프로젝트에서는 다소 과할 수 있습니다.
  2. 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. 한계 및 주의점

  1. 복잡한 쿼리: 매우 복잡한 조합은 코드가 길어질 수 있어, QueryDSL이 더 적합한 경우가 있습니다.
  2.  
  3. 초기 학습 곡선: JPA Criteria API와의 친숙함이 필요합니다.
  4.  
  5. 성능 주의: 불필요한 조건 조합은 성능 문제를 초래할 수 있습니다.

 

 

 

 

 

 

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);
	}


	

 

 

 

 

 

 

 

 

 

 

 

 

spring

 

about author

PHRASE

Level 60  라이트

홧김에 서방질한다 , 화가 나면 차마 못 할 짓도 한다는 말.

댓글 ( 0)

댓글 남기기

작성