JPA Search By Condition(4) - Specification

GEONNY·2024년 8월 25일
0
post-thumbnail

이전에 알아보았던 Criteria API 는 단순히 참/거짓을 리턴하는 Predcate 를 조합하여 여러 조건 조회를 구현했습니다. Spring-data-jpa 는 Specification interface 를 제공함으로써, 보다 쉽게 이를 활용할 수 있게 제공하고 있습니다.

📌Specification

Specificationorg.springframework.data.jpa.domain 패키지에 정의된 interface 로 JPA Criteria API를 기반으로 동적 쿼리를 작성하기 위해 사용됩니다.

📍toPredicate

toPredicate 는 Specification 의 주요 메서드로 쿼리 조건을 설정하는 Predicate 를 리턴합니다.

Predicate toPredicate(Root<T> root,
					  CriteriaQuery<?> query, 
                      CriteriaBuilder criteriaBuilder);

쿼리에서 활용할 주 Entity 인 root로 Entity의 field 에 접근할 수 있고, CriteriaBuilder를 사용해서 쿼리에서 사용되는 다양한 조건(Predicate)을 생성할 수 있습니다. CriteriaQuery 는 쿼리 자체를 나타내며, 쿼리를 구성하는 데 필요한 정보를 제공합니다.

📍조건 연결

Specification 은 and, or, not, where, allOf, anyOf 기본 메소드를 제공하며, 이 메소드들을 통해 여러 개의 Specification을 조합할 수 있습니다. 이를 통해 보다 복잡한 쿼리를 쉽게 생성할 수 있습니다.

📌JpaSpecificationExecutor

Specification 을 사용하기 위해선 Repository 에 JpaSpecificationExecutor<T> 를 상속받아야 합니다.

@Repository
public interface MemberSpecificationRepository extends JpaRepository<Member, String>, 
													   JpaSpecificationExecutor<Member> {
}

JpaSpecificationExecutor interface 에는 Specification 을 매개변수로 받는 다양한 메서드를 제공합니다.

📌MemberSpecification

memberId, memberName 조건을 설정하여 Specification<Member> 를 리턴하는 static method 를 생성하겠습니다. 이렇게 작성해 놓으면 조건 필요 시 언제든 재활용이 가능하고 가독성도 높여줍니다.

public class MemberSpecification {

    public static Specification<Member> memberId(String memberId) {
        return (root, query, criteriaBuilder) ->
                (StringUtils.isEmpty(memberId)) 
                	? criteriaBuilder.conjunction() 
                    : criteriaBuilder.equal(root.get("memberId"), memberId);
        
    }

    public static Specification<Member> memberName(String memberName) {
        return (root, query, criteriaBuilder) ->
                (StringUtils.isEmpty(memberName)) 
                	? criteriaBuilder.conjunction()
                    : criteriaBuilder.equal(root.get("memberName"), memberName);
    }
}

📍CriteriaBuilder.conjuction()

conjuction method 는 항상 true 인 Predicate를 리턴하는 메서드로 주로 동적 쿼리에서 초기 조건을 설정하거나 And 조건을 누적할 때 유용하게 사용됩니다. null 을 리턴할 경우 NPE 이 발생할 수 있기 때문에 항상 true인 Predicate를 전달함으로써, 조건 조합을 유연하게 할 수 있습니다.

📍CriteriaBuilder.disjunction()

conjuction과 반대되는 개념으로 항상 false 인 Predicate를 리턴합니다. 이것은 Or 조건을 조합할 때 주로 사용됩니다.

📍참고) Member Entity

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table
@Entity(name = "member")
public class Member extends BaseEntity implements Persistable<String> {

    @Id
    @Column(name = "member_id")
    private String memberId;

    @Column(name = "member_pw")
    private String password;

    @Column(name = "member_nm")
    private String memberName;

    @Column(name = "use_yn")
    @Enumerated(EnumType.STRING)
    private UseYn useYn;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "authority_cd")
    private Authority authority;
    //이하 생략..

📌MemberSpecificationSerivce

작성한 Specification 을 활용해 보겠습니다.

@Service
@RequiredArgsConstructor
public class MemberSpecificationServiceImpl {

    private final MemberSpecificationRepository memberRepository;
    private final MemberMapper memberMapper = Mappers.getMapper(MemberMapper.class);

    public List<MemberSearchResponse> getMemberByIdAndName(String memberId, String memberName) {
        List<Member> members = memberRepository.findAll(
                MemberSpecification.memberId(memberId)
                        .and(MemberSpecification.memberName(memberName))
        );
        return memberMapper.toRecordList(members);
    }
}

JpaSpecificationExecutor 를 상속받은 MemberSpecificationRepository 의 findAll method를 호출합니다. 위에서 생성한 MemberSpecification의 조건 method 를 findAll method의 매개변수로 전달함으로써 조건 조회를 구현합니다.

📌Sorting

정렬은 org.springframework.data.domain.Sort 를 사용합니다. JpaRepository와 JpaSpecificationExecutor 의 구현체들 중에는 Sort 를 매개변수로 받는 메서드들이 있습니다. 필요에 따라 선택하여 사용합니다.

@Transactional
public List<MemberSearchResponse> getMemberOrderByMemberNameAsc() {
    Sort sort = Sort.by(Sort.Order.asc("memberName"));
    List<Member> members = memberRepository.findAll(sort);
    return memberMapper.toRecordList(members);
}

📌Paging

페이징 처리는 rg.springframework.data.domain.Pageable 을 사용합니다. Pageable을 매개변수로 받는 findAll method 는 위 Sorting 의 이미지를 참고하세요.
아래 코드는 memberName 으로 정렬한 첫 번째 페이지 10개의 데이터를 조회하는 코드 입니다. Method 를 실행하면, 페이지 관련 쿼리와 전체 데이터 개수 조회 쿼리가 실행 됩니다. (전체 페이지 개수를 알아야 페이징 처리 가능)

@Transactional
public List<MemberSearchResponse> getMemberOrderByMemberNameAscWithPaging() {
    Sort sort = Sort.by(Sort.Order.asc("memberName"));
    Pageable pageable = PageRequest.of(0, 10, sort);
    Page<Member> members = memberRepository.findAll(pageable);
    return memberMapper.toRecordList(members.getContent());
}

📌Conclusion

Specification 보통 조회 조건을 미리 작성해 놓고, 필요에 따라 조합해서 활용합니다. 조건의 재사용이 가능하고 동적으로 연결해 사용할 수 있는 장점이 있지만, 조건을 미리 생성해 두어야 하고, 많거나 중첩된 경우, 여러 Specification 의 조합으로 구조가 복잡해져 가독성이 떨어질 수 있습니다. 프로젝트의 난이도나 요구사항에 따라 적절히 다른 조건 조회(Query mehtod, @Query, Criteria API, DSL 라이브러리)와 비교하여 사용하도록 합시다.

📚참고

📕Page, Pageable

Spring-data-jpa 의 페이징과 정렬 기능을 지원하는 인터페이스 입니다.

📖Pageable

페이징 정보를 캡슐화하는 인터페이스로, 어떤 페이지의 어떤 크기의 데이터를 요청할지에 대한 정보를 담고 있습니다. 또한 정렬 정보도 포함할 수 있습니다. 위에 예시에서 활용한 PageRequest 가 가장 일반적인 구현체 입니다.
Pageable 주요 메서드
int getPageNumber() : 현재 페이지 번호 반환 (0~)
int getPageSize() : 페이지당 row 수를 반환
Sort getSort() : 정렬 정보 반환
Pageable next() : 다음 페이지 정보 반환
Pageable previousOrFirst() : 이전, 또는 첫 번째 페이지 정보 반환
Pageable first() : 첫 번째 페이지 정보 반환
boolean hasPrevious() : 이전 페이지 존재여부 반환

📖Page

페이징된 결과 데이터를 표현하는 인터페이스로, 조회된 데이터와 페이징 관련 메타데이터를 포함합니다.
Page 주요 메서드
int getTotalPages() : 전체 페이지 수 반환
long getTotalElements() : 전체 row 수 반환
List<T> getContent() : 현재 페이지의 데이터 List 반환
int getNumber() : 현재 페이지 번호 반환
int getSize() : 페이지당 표출 row 수 반환
int getNumberOfElements() : 현재 페이지의 row 수 반환
Sort getSort() : 정렬 정보 반환
boolean hasContent() : 데이터 존재 여부 반환
boolean isFirst() : 첫 번째 페이지 여부 반환
boolean isLast() : 마지막 페이지 여부 반환
boolean hasNext() : 다음 페이지 여부 반환
boolean hasPrevious() : 이전 페이지 여부 반환
Pageable nextPageable() : 다음 페이지 Pageable 객체 반환
Pageable previousPageable() : 이전 페이지 Pageable 객체 반환

📖Slice

Page 와 유사하지만 전체 데이터의 개수나 전체 페이지 수에 대한 정보가 빠져 가벼운 인터페이스 입니다.
전체 개수나, 전체 페이지 개수가 필요없고, 가볍고 빠른 페이징 정보를 원하면 Page 대신 Slice를 활용하면 됩니다. Page interface 가 Slice 를 상속받기 때문에 기존의 Page<T> 를 리턴하는 Repository 는 수정하지 않아도 됩니다.
org.springframework.data.domain.Page

public interface Page<T> extends Slice<T> {
//생략
}
@Transactional
public List<MemberSearchResponse> getMemberOrderByMemberNameAscWithPagingSlice() {
    Sort sort = Sort.by(Sort.Order.desc("memberName"));
    Pageable pageable = PageRequest.of(0, 10, sort);
    Slice<Member> members = memberRepository.findAll(pageable);
    return memberMapper.toRecordList(members.getContent());
}
profile
Back-end developer

0개의 댓글