저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

'specifications'에 해당되는 글 1건

  1. 2012.03.16 Spring Data JPA의 Specifcation을 이용한 검색 조건 조합의 편리함 (6)
JPA를 사용할 때 입력 값에 따라 동적으로 검색 조건을 변경하려면 Criteria API를 사용하면 되지만, 이게 참 이쁘지가 않고 코드를 만지다 보면 짜증이 나기도 한다. 최근에 하는 프로젝트에서 Spring Data JPA를 선택했는데, Spring Data JPA 덕분에 검색 조건을 조합할 때 발생하는 짜증을 많이 감소시킬 수 있게 되었다. Spring Data JPA가 Repository 또는 Dao의 인터페이스만 정의하면 알아서 구현체를 만들어준다는 장점이 정말 좋은데, 그에 못지 않게 검색 조건을 추상화한 Specification을 지원한다는 것 역시 아주 마음에 든다.

Specification을 사용하는 방법은 아주 간단한데, 그 방법은 다음과 같다.
  1. Specification을 입력 받도록 Repository 인터페이스를 정의하기
  2. 검색 조건을 모아 놓은 클래스 만들기
  3. 검색 조건을 조합한 Specification 인스턴스를 이용해서 검색하기
하나씩 만들어보자.

1. Specification을 입력 받는 Repository 메서드 정의
Spring Data에서 Specification은 검색 조건을 추상화하기 위해 사용되는데, 이 추상화된 검색 조건을 이용해서 데이터를 검색하도록 구현하려면 다음과 같이 Specification을 검색 메서드의 파라미터로 추가해주기만 하면 된다.

public interface ContentRepository extends Repository<Content, Long> {

Page<Content> findAll(Specification<Content> spec, Pageable pageable);

}


2. 검색 조건 모아 놓은 클래스 만들기
다음에 할 일은 검색 조건을 모아 놓은 클래스를 하나 만드는 것이다. 이 클래스를 구현할 때 JPA의 Criteria API를 사용하게 된다. 아래는 구현 예이다.

public class ContentSpecs {


    public static Specification<Content> titleLike(final String keyword) {

        return new Specification<Content>() {

            @Override

            public Predicate toPredicate(Root<Content> root,

                    CriteriaQuery<?> query, CriteriaBuilder cb) {

                return cb.like(root.get(Content_.title), "%" + keyword + "%");

            }

        };

    }


    public static Specification<Content> category(final Category category) {

        return new Specification<Content>() {

            @Override

            public Predicate toPredicate(Root<Content> root,

                    CriteriaQuery<?> query, CriteriaBuilder cb) {

                return cb.equal(root.get(Content_.category), category);

            }

        };

    }

    ... // 기타 검색 조건 생성 클래스

}


위에서 눈여겨 볼 점은 Criteria를 이용해서 검색 조건을 지정하는 코드가 category(), titleLike()와 같은 메서드로 추상화된다는 점이다.

3. 검색 조건 조합해서 검색하기
이제 할 일은 앞서 만든 검색 조건을 모아 놓은 클래스를 이용해서 검색 조건을 조합하고 Repository를 이용해서 결과를 가져오는 것이다. 아래는 예시 코드이다.

Specifications<Content> spec = Specifications.where(ContentSpecs.titleLike(query));
if (category != null) {
     spec = spec.and(ContentSpecs.category(category));
}
Page<Content> p = contentRepository.findAll(spec); // david hong 님의 지적으로 수정합니다.


Spring Data JPA가 제공하는 Specifications를 사용하면 두 개 이상의 Specification을 AND나 OR 등으로 조합할 수 있다.


왜 좋지?
이 방식이 마음에 드는 이유는 검색 조건을 생성할 때 도메인의 용어를 사용할 수 있게 된다는 점이다. 앞서 만들었던 ContentSpecs.titleLike()만 보더라도 이 검색 조건이 제목을 LIKE 검색하기 위한 조건이라는 것을 알 수 있다.

마음에 드는 또 다른 이유는 검색 조건을 조합하는 코드에서 Criteria와 같은 실제 구현 기술의 타입을 사용하지 않는다는 점이다. 사용자는 ContentSpecs.category() 메서드를 사용해서 검색 조건을 추상화한 Specification 객체를 구하기만 하면 된다. 내부적으로 JPA의 Criteria를 사용하는지의 여부는 전혀 알 필요가 없다.

마지막으로 이 방식은 누구나 쉽게 만들 수 있을 만큼 쉽다. 처음 이 방식을 접하는 경력자 뿐만 아니라 이제 3개월 된 신입들도 별다른 어려움 없이 위 방식에 적응했다.

불필요한 코드 작성을 줄이고 그러면서도 이전보다 쉽게 DB 연동 부분을 처리하고 싶은 개발자분들에게 JPA + Spring Data JPA/Specification의 조합을 아주 강력하게 권장한다. 


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. david hong 2013.03.15 16:53 신고  댓글주소  수정/삭제  댓글쓰기

    spec 설정과 repository 의 findAll 호출간에 relation 이 없는데 findAll(spec,pageRequest) 와 같은 형태로 전달해야 하지 않나요?

    • 최범균 madvirus 2013.03.17 20:38 신고  댓글주소  수정/삭제

      글에 오류가 있었네요. findAll(pageRequest)가 아니라 findAll(spec) 이었습니다. 지적하신 부분 수정해 놓았습니다. 잘못된 부분 알려주셔거 고맙습니다.

  2. ncrash 2013.12.06 17:10 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 잘 보고 있습니다 ^^

    저의 개발 환경에서는 Hibernate Metamodel generator를 사용하지 않고 개발했던 터라
    Content_ 처럼 자동생성 되는 클래스 없어서 해매다 답은 찾아서 적용하고 있습니다

    http://stackoverflow.com/questions/8634241/how-to-make-a-custom-search-by-spring-data-jpa-framework/8634316#8634316


    헌데 Specifications를 통해 적용하는데 한가지 문제가 있어 질문 드립니다.
    where에 대한 조건은 적용이 잘 됩니다.

    헌데 and조건 부터는 먹지를 않네요.. 하이버네이트 버전 문제인가 싶어 버전을 바꿔봐도 동일하구요.

    Specifications<TransactionLog> spec = Specifications.where(TransactionLogSpecs.startedDateBetween(dt.minusDays(1).toDate(), dt.toDate()));
    spec.and(TransactionLogSpecs.AppIdLike("asdf"));

    • 최범균 madvirus 2013.12.06 18:16 신고  댓글주소  수정/삭제

      and()를 호출해줄 때 마다 새로운 객체가 생성됩니다. (기존 정보가 변경되는 게 아니죠.) 그래서 아래와 같이 and() 호출해 줄 때 마다 할당을 새로 해 주어야 합니다.

      spec = spec.and(....);
      spec = spec.and(....);

  3. ncrash 2013.12.08 11:15 신고  댓글주소  수정/삭제  댓글쓰기

    답변을 엄청 빨리 달아주셨는데 이제서야 확인했네요
    답변 감사 드립니다 ^^

    전 StringBuilder.appned() 메소드와 동일한 형태로 처리되는 줄 알았는데 헛다리를 ㅠ.ㅠ

    그렇다면 위 포스팅 내용도 아래와 같이 변경되야 하는거 아닌지 궁금하네요

    if (category != null) {
    spec = spec.and(ContentSpecs.category(category));
    }