make it simple
article thumbnail

JPA 에서는 여러가지 동적 쿼리를 할 수 있는 방법들이 주어진다. JPQL,Specification,QueryDSL,Criteria 등이 있다. 그 중에 Specification 에 대해 알아보자. (보편적으로 실무에서는 가독성 & 간편함 때문에 Specification보단 QueryDSL을 많이 쓴다. 그냥 이런 기술이 있구나 하고 알아두면 좋을 거 같다.)

 

User 엔티티

@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue
    private Long id;
    private Integer age;
    private String userId;
    private String nickName;
  }

Specification이란?

  • 검색 조건을 다양하게 조합하며 조회할 때 사용할 수 있는 것이 specification이다. 
  • specification인테페이스는 밑처럼 구현되어있다. isSatisfiedBy(T agg) 는 agg는 검사 대상이 되는 객체고 검색 결과를 충족하면 true를 리턴하며 검색조건에 맞게 데이터가 리턴된다. 
public interface Speficiation<T> { 
	public boolean isSatisfiedBy(T agg);
}

UserSpecs 클래스 

  • User 엔티티에 있는 userId, nickname, age의 범위를 검색할 수 있게 specification들을 UserSpecs라는 한 클래스에 모았다.
public class UserSpecs {
    public static final String USERID = "userId";
    public static final String NICKNAME = "nickname";
    public static final String AGE = "age";

    // 유저 아이디 검색
    public static Specification<User> userId(String userId) {
        return (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb)
                -> cb.equal(root.get(USERID), userId);
    }
    
    // 닉네임 검색
    public static Specification<User> nickname(String nickname) {
        return (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb)
                -> cb.equal(root.get(NICKNAME), nickname);
    }

    // 나이 검색
    public static Specification<User> age(int minAge, int maxAge) {
        return (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb)
                -> cb.between(root.get(AGE), minAge, maxAge);
    }
}

UserRepository

public interface UserRepository extends JpaRepository<User, Id> {
    List<User> findAll(Specification<User> spec);
}

Test

단일 Specifiaction 넘기기

   @Test
    public void spec_조회_테스트() {  
        // give
        Specification<User> ageSpec = UserSpecs.age(20, 30);     
        // when
        List<User> users = service.getUsers(ageSpec);
        // then 
        Assertions.assertEquals(users.size(),4);
    }

  • 위처럼 테스트 코드를 작성했다. 위에 Specfication을 모아둔 UserSpecs클래스에 나이 specfication을 만든 후 Service 메소드에 넘기면 Repository에 넘겨서 데이터를 리턴받는다. 위 처럼 쿼리가 잘 동작 되었다.

다중 Specification 넘기기

@Test
public void spec_조회_테스트() {
    // given
    Specification<User> ageSpec = UserSpecs.age(20, 30);
    Specification<User> userIdSpec = UserSpecs.userId("test_user2");
    Specification<User> specs = ageSpec.and(userIdSpec);
    // when
    List<User> users = service.getUsers(specs);
    // then
    Assertions.assertEquals(users.size(),1);
}

  • 여러개의 Specfication 객체를 만든 후 하나의 Specification 객체에다가 .and()나 .or() .not() 등등의 조건을 붙혀서 여러개의 specification으로 조회할 수 있다. 위 처럼 쿼리가 잘 동작 되었다.

Spring Data JPA 정렬 방법

 

메서드 이름에 OrderBy를 사용해서 정렬 기준 지정

public interface UserRepository extends JpaRepository<User, Id> {
    List<User> findAllByOrderByIdDesc();
}
  • 무조건 findAllByOrder{컬럼이름}Desc(); 이런식으로 네이밍 해줘야한다. (DESC -> 내림차순, ASC -> 오름차순)
  • 위처럼 정렬기준이 많아지면 메소드명이 길어지고 정렬 순서가 정해지기 떄문에 상황에따라 정렬 순서를 변경할 수 없다. 이럴 땐 Sort를 쓰자.

Sort를 Parameter로 전달

import org.springframework.data.domain.Sort;

public interface UserRepository extends JpaRepository<User, Id> {
    List<User> findAll(Sort sort);
}
@Test
public void sort테스트(){
    
    // 1개의 정렬 기준
    Sort sort = Sort.by("id").descending();
    
    // 2개 이상 정렬 기준 
    Sort sortById = Sort.by("id").descending();
    Sort sortByNickName = Sort.by("nickName").descending();
    Sort sortByAll = sortById.and(sortByNickName);
    
    List<User> users = userRepository.orderBySort(sort);
}
  • 위와 같이 sort객체를 만든 후 Sort.by()안에 원하는 컬럼 이름을 넣은 후 repository에 넘겨주면 된다. 여러개의 정렬기준도 위처럼 사용가능하다.

페이징 처리

import org.springframework.data.domain.Pageable;
import org .springframework.data.domain.Page;

public interface UserRepository extends JpaRepository<User, Id> {
    Page<User> findAllBy(Pageable pageable);
}
@Test
public void 페이징테스트(){
    PageRequest pageReq = PageRequest.of(1, 10);
    Page<User> users = service.findAllByPageable(pageReq);
    
    List<User> content = users.getContent();
    int totalElements = users.getTotalElements();
}
  • 위처럼 Pageable 객체를 repository에서 parameter로 받으면 된다. 
  • 테스트 코드에서 보면 Pageable 타입은 인터페이스로 실제 Pageable 타입 객체는 PageRequest 클래스를 이용해서 생성한다. PageRequest.of() 메서드의 첫 번째 인자는 페이지 번호, 두번째 인자는 한페이지의 개수를 의미한다. 페이지는 0부터 시작함으로 위는 한 페이지에 10개씩 표시하고 두번째 페이지를 조회한다. 즉 11번째부터 20번째의 데이터를 조회한다.
  • Page객체로 감싸면 조건에 해당하는 전체 개수(totalElements- 페이징 처리에 필요)와 조회 결과 목록(content - 페이징 처리된 데이터)을 알 수 있다.

결론

  • 간단하게 Spring Data JPA로 specification을 이용한 동적쿼리 sort객체를 활용해 정렬 기준 pageable객체를 이용한 페이징 처리를 해봤다. sort나 pageable은 실무에서도 사용한다. 하지만 specification은 criteria 기반으로 작성되는데 요구사항이 복잡해지거나하면 코드가 많이 복잡해지고 가독성이 떨어진다. 그래서 실무에서 동적 쿼리를 구현할때는 활용도나 재활용성이 높은 QueryDSL을 많이 사용한다. specification은 그냥 이런기술이 있구나 하고 알고 넘어가고 실무에서 사용을 지양한다. ( 참고: 인프런 김영한 선생님)

reference

profile

make it simple

@keep it simple

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!