빈쿵바라기
좌충우돌 개발자의 기록
빈쿵바라기
전체 방문자
오늘
어제
  • 분류 전체보기 (53)
    • Programming (25)
      • JAVA (12)
      • Spring Boot (6)
      • JPA (7)
      • Python (0)
    • Database (12)
      • RDBMS (4)
      • NoSQL (7)
    • Server (11)
    • Elasticsearch (3)
    • ETC (2)

블로그 메뉴

    공지사항

    인기 글

    최근 댓글

    최근 글

    티스토리

    hELLO · Designed By 정상우.
    빈쿵바라기
    Programming/JPA

    [Spring Data JPA] 유연한 조회 API만들기(Specification)

    [Spring Data JPA] 유연한 조회 API만들기(Specification)
    Programming/JPA

    [Spring Data JPA] 유연한 조회 API만들기(Specification)

    2022. 8. 17. 16:56

    저번 포스터에서 Query를 사용하는 여러가지 방법에 대해서 알아 봤습니다. 데이터를 조회하는 API를 구현할 때, 다양한 검색 조건을 처리할 수 있어야 합니다. 이렇게 유연하게 처리할 수 있게 도와주는 JPA Specificcation에 대해서 알아봅시다.

    1. JPA Specification 이란?

    JPA Spectification은 DB 쿼리의 조건을 Spec으로 작성하여 요청할 수 있게 해줍니다. 다양한 조건이 계속 추가 될 수록 Spec으로 관리하게 되면 코드가 깔끔해지고 유지보수가 좀 더 용이해집니다.


    2. Specification 구현 준비

    JPA Specification을 구현하기 위해서는 Repository에 JpaSecificationExecutor<T>를 추가로 상속 받아야합니다.

    public interface MemberRepository extends JpaRepository<Member, Integer>, JpaSpecificationExecutor<Member> {
    }

    상속한 JpaSpecificationExecutor 인터페이스를 살펴보자.

    public interface JpaSpecificationExecutor<T> {
    
    	Optional<T> findOne(@Nullable Specification<T> spec);
    
    	List<T> findAll(@Nullable Specification<T> spec);
    
    	Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
    
    	List<T> findAll(@Nullable Specification<T> spec, Sort sort);
    
    	long count(@Nullable Specification<T> spec);
    }

    인터페이스에 있는 모든 메소드는 매개변수로 Specification<T>를 받습니다. 우리는 이 Specification을 구현해서 파라미터를 넣어주면 되는데, 아래 내용을 더 살펴 보시면 Specification을 통해 유연한 쿼리를 작성할 수 있게 됩니다.


    3. Specification 구현

    이제 Specification을 구현해봅시다. 엔티티의 여러 변수중에서 하나의 컬럼에 하나의 조건을 사용하여 검색하려고합니다. 아래는 간단하게 Member 엔티티의 memberName 컬럼에 equal로 검색하는 코드입니다.

    public MemberSpecification {
    
    	public static Specification<Member> equalName(String name) {
    		return (Specification<Member>) ((root, query, builder) -> 
    			builder.equal(root.get("memberName"), name)
    		);
    	}
    }

    root.get("memberName")을 통해 Member 엔티티의 memberName 컬럼에 매개변수 name과 일치하는 데이터를 찾아 Member 객체를 반환합니다. 이렇게 작성한 Specification을 이용하여 조회하는 방법은 아래와 같습니다.

    memberRepository.findAll(MemberSpecification.equalName("John"));

    예제는 equal()만 보았지만, 이외에도 greaterThan(), lessThan(), like(), in(), not(), isNull() 등 다양한 메소드를 지원합니다. 


    4. Specification 응용

    위의 예제에서 간단하게 엔티티의 하나의 필드에 어떤 조건으로 검색할지 알아봤는데, 위에 코드에는 크게 2가지 문제가 있습니다. 

    • 여러개의 컬럼에 equal 검색하고 싶을 때
      • 각 컬럼마다 eqaul 메소드를 구현 해야하는 번거러움 발생
    • equal()이외에도 다른 메소드(greateThan, like, in 등)를 이용하여 검색하고 싶을 때
      • 각각의 메소드마다 구현해야하며, 여러 컬럼에 대하여도 구현할 때 엄청난 코드 작성 필요
    실무에서 활용하기 위해서는 여러개의 컬럼을 수용할 수 있으며, 여러 메소드를 동시에 사용할 수 있어야 한다.

    이제 위의 문제를 보완한 응용코드를 보겠습니다. 먼저 어떤 컬럼에 어떤 메소드를 어떤 값으로 조회할지 검색 조건 정보를 가지는 model를 만들겠습니다.

    public class Filter {
    	// 엔티티 속성명(컬럼)
    	String column;
    	// 값
    	Object value;
    	// 연산
    	Operation operation;
        
    	//@Getter, @Setter
    }
    
    public enum Operation {
    	GT, // GREATER_THAN
    	LT, // LESS_THAN
    	GTE, // GREATER_THAN_EQUAL
    	LTE, // LESS_THAN_EQUAL
    	NOT_EQUAL,
    	EQUAL,
    	LIKE
    	IN,
    	NOT_IN,
    	IS_NULL,
    	IS_NOT_NULL
    }

    column 변수는 엔티티의 속성명을 가지고, value는 검색하고자 하는 값, operation은 연산자정보를 가지는 Enum 변수입니다. Filter를 List로 받아서 처리 할 수 있는 메소드를 만들겠습니다.

    public class CustomSpecification {
    
    	public static Specification<T> bySearchQuery(List<Filter> filters, Class<T> clazz) {
    		return (root, query, builder) -> {
    			List<Predicate> predicates = new ArrayList<>();
    
    			for(final Filter filter : filters) {
    				switch (filter.getOperation()) {
    					case GT: 
    						predicates.add(builder.greaterThan(root.get(filter.getColumn()), filter.getValue().toString()));
    						break;
    					case LT:
    						predicates.add(builder.lessThan(root.get(filter.getColumn()), filter.getValue().toString()));
    						break;
    					case GTE:
    						predicates.add(builder.greaterThanOrEqualTo(root.get(filter.getColumn()), filter.getValue().toString()));
    						break;
    					case LTE:
    						predicates.add(builder.lessThanOrEqualTo(root.get(filter.getColumn()), filter.getValue().toString()));
     						break;
    					case NOT_EQUAL:
    						predicates.add(builder.notEqual(root.get(filter.getColumn()), filter.getValue().toString()));
    						break;
    					case EQUAL:
    						predicates.add(builder.equal(root.get(filter.getColumn()), filter.getValue().toString()));
    						break;
    					case LIKE:
    						predicates.add(builder.like(root.get(filter.getColumn()), filter.getValue().toString()));
    						break;
    					case IN:
    						predicates.add(builder.in(root.get(filter.getColumn())).value(filter.getValue()));
    						break;
    					case NOT_IN:
    						predicates.add(builder.not(root.get(filter.getColumn())).in(filter.getValue()));
    						break;
    					case IS_NULL:
    						predicates.add(builder.isNull(root.get(filter.getColumn())));
    						break;
    					case IS_NOT_NULL:
    						predicates.add(builder.isNotNull(root.get(filter.getColumn())));
    					break;
    				}
    			}
    
    			return builder.and(predicates.toArray(new Predicate[0]));
    		};
    	}
    }

    여러 검색 조건을 가지는 List<Filter>를 받아서 처리하는 코드를 만들었습니다. 이제 CustomSpecification.bySearchQuery()를 호출해서 사용해보겠습니다.

    public class MemberService {
    
    	@Autowired private MemberRepository memberRepository;
    	
    	public void getList(){
    		List<Filter> filters = new ArrayList<Filter>();
            
    		// 조건1 : memberName LIKE '%홍%'
    		Filter filter1 = new Filter();
    		filter.setKey("memberName");
    		filter.setValue("%홍%");
    		filter.setOperation(Operation.LIKE);
    		filters.add(filter1);
            
    		// 조건2 : age > 15
    		Filter filter2 = new Filter();
    		filter.setKey("age");
    		filter.setValue(15);
    		filter.setOperation(Operation.GT);
    		filter.add(filter2);
            
    		// 검색
    		List<Member> list = memberRepository.findAll(CustomSpecification.bySearchQuery(filters, Member.class));
    	}
    }
    저작자표시 (새창열림)

    'Programming > JPA' 카테고리의 다른 글

    [Spring Data JPA] Query를 사용하는 여러가지 방법  (0) 2022.08.11
    [Spring Data JPA] FetchType - 즉시 로딩(EAGER)과 지연 로딩(LAZY)  (0) 2022.08.04
    [Spring Data JPA] 양방향 순환참조 문제 및 해결방법  (0) 2022.08.03
    [Spring Data JPA] JPA 연관관계 매핑(JOIN)  (0) 2022.08.02
    [Spring Data JPA] JPA 주요 어노테이션(Annotation)  (0) 2022.07.31
      'Programming/JPA' 카테고리의 다른 글
      • [Spring Data JPA] Query를 사용하는 여러가지 방법
      • [Spring Data JPA] FetchType - 즉시 로딩(EAGER)과 지연 로딩(LAZY)
      • [Spring Data JPA] 양방향 순환참조 문제 및 해결방법
      • [Spring Data JPA] JPA 연관관계 매핑(JOIN)
      빈쿵바라기
      빈쿵바라기
      삽질하는 개발자의 좌충우돌 개발기

      티스토리툴바

      단축키

      내 블로그

      내 블로그 - 관리자 홈 전환
      Q
      Q
      새 글 쓰기
      W
      W

      블로그 게시글

      글 수정 (권한 있는 경우)
      E
      E
      댓글 영역으로 이동
      C
      C

      모든 영역

      이 페이지의 URL 복사
      S
      S
      맨 위로 이동
      T
      T
      티스토리 홈 이동
      H
      H
      단축키 안내
      Shift + /
      ⇧ + /

      * 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.