본문 바로가기

개발중/Java Persistence API (JPA)

[Spring] JPA Specification 를 이용해 검색 API 개발하기 ✍

728x90
반응형

 

일단, 저는 JPA 에 대해 매우 큰 관심이 있습니다.

하지만 우리 회사에서는 사용하지 않기에, JPA 에 대해 공부를 해보려고 합니다 !

 

Spring Jpa Progect Ex ✍

 

GitHub - soobinJung/Jpa_Defalut: Spring Jpa Specification Test

Spring Jpa Specification Test. Contribute to soobinJung/Jpa_Defalut development by creating an account on GitHub.

github.com

 

아래와 같은 결과 값을 반환하는 간단한 API 를 구상해보았습니다.

 

더보기
http://localhost:8080/api/user

[
    {
        "id": 1,
        "userName": null,
        "password": "1234",
        "email": "soo@nnn"
    }
    , {
        "id": 2,
        "userName": null,
        "password": "1234",
        "email": "soo@nnn"
    }
]

 

http://localhost:8080/api/user/1

[
    {
        "id": 1,
        "userName": null,
        "password": "1234",
        "email": "soo@nnn"
    }
]

구현

정말 간단한 도메인, 레파지토리, 서비스, 컨트롤 구현

더보기

Domain

package com.example.binsoojpa.domain;

import lombok.*;
import javax.persistence.*;

@Getter
@Builder
@EqualsAndHashCode(callSuper = false, of = "id")
@AllArgsConstructor
@NoArgsConstructor
@Table(name="BINSOO_USER")
@Entity
public class BinsooUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    long id;

    @Column(name = "userName")
    String userName;

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

    @Column(name = "email")
    String email;
}

 

Repository

package com.example.binsoojpa.repository;

import com.example.binsoojpa.domain.BinsooUser;
import org.springframework.data.jpa.repository.JpaRepository;

public interface JpaRepository extends JpaRepository<BinsooUser, Long> {
    public BinsooUser findById( long id );
}

 

Service

package com.example.binsoojpa.service;

import com.example.binsoojpa.domain.BinsooUser;
import com.example.binsoojpa.repository.JpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
@RequiredArgsConstructor
public class JpaService {

    private final JpaRepository repository;

    public List<BinsooUser> findAll (){
        return repository.findAll();
    }

    public BinsooUser findById (long id){
        return repository.findById(id);
    }
}

 

Controller

package com.example.binsoojpa.controller;

import com.example.binsoojpa.domain.BinsooUser;
import com.example.binsoojpa.service.JpaService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class JpaController {

    private final JpaService service;

    @GetMapping("/user")
    public List<BinsooUser> findAll (){
        return service.findAll();
    }

    @GetMapping("/user/{id}")
    public BinsooUser findById(@PathVariable long id){
        return service.findById(id);
    }
}

 


사실 위에와 같이 단순히 리스트와 단건 조회를 하는 것은 쉬운일이지만,

프로젝트가 커질 수록 다양한 조건에 대응해야 하는 상황에서는 결코 쉽지 않습니다.

 

하지만 Spring Jpa 에서는 Specification 이라는 동적 쿼리를 사용하기 위한 라이브러리를 지원합니다.

Specification 는 criteria API 를 기반으로 만들어졌습니다.

JPA Criteria 도 마찬가지로 동적 쿼리를 사용하기 위한 JPA 라이브러리입니다.


Specification 를 사용하려면 위에 구현되어있던 Repository 에 JpaSpecificationExecutor 를 구현받아야합니다.
package com.example.binsoojpa.repository;

import com.example.binsoojpa.domain.BinsooUser;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.JpaRepository;

public interface JpaRepository extends JpaRepository<BinsooUser, Long>, JpaSpecificationExecutor<BinsooUser> {
    public BinsooUser findById( long id );
}

spac 을 구현해서 검색 조건에 대한 부분을 분리시켜 개발하였습니다.

 

공통 Spec

더보기
package com.example.binsoojpa.common.spec;

import java.util.ArrayList;
import java.util.List;

import org.springframework.data.jpa.domain.Specification;

public class SpecBuilder {
    public static<T> Builder<T> builder(Class<T> type) {
        return new Builder<T>();
    }

    public static class Builder<T> {
        private List<Specification<T>> specList = new ArrayList<>();

        public Builder<T> addSpec(Specification<T> spec) {
            specList.add(spec);
            return this;
        }

        public Specification<T> toSpecification() {
            if (specList.isEmpty()) return Specification.where(null);
            else if (specList.size() == 1) return specList.get(0);
            else {
                Specification<T> spec = specList.get(0);
                specList.remove(0);
                return specList.stream()
                        .reduce(
                                spec, (specOne, specTwo) -> specOne.and(specTwo)
                        );
            }
        }
    }
}

 

BinsooUser Spec

더보기
package com.example.binsoojpa.spec;

import com.example.binsoojpa.domain.BinsooUser;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

public class JpaSpec {

    public static Specification<BinsooUser> userNameLike(final String userName) {
        return new Specification<BinsooUser>() {
            public Predicate toPredicate(Root<BinsooUser> root, CriteriaQuery<?> query, CriteriaBuilder builder ) {
                if(userName == null || "".equals(userName) ) return builder.conjunction();
                return builder.like(root.get("userName"), new StringBuilder().append("%").append(userName).append("%").toString());
            }
        };
    }

    public static Specification<BinsooUser> emailLike(final String email) {
        return new Specification<BinsooUser>() {
            public Predicate toPredicate(Root<BinsooUser> root, CriteriaQuery<?> query, CriteriaBuilder builder ) {
                if(email == null || "".equals(email) ) return builder.conjunction();
                return builder.like(root.get("email"), new StringBuilder().append("%").append(email).append("%").toString());
            }
        };
    }
}

검색 조건 전달 객체 구현

더보기
package com.example.binsoojpa.domain;

import com.example.binsoojpa.common.spec.SpecBuilder;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.jpa.domain.Specification;
import static com.example.binsoojpa.spec.JpaSpec.*;
import static org.springframework.data.jpa.domain.Specification.where;
@Getter
@Setter
public class BinsooSearch{

    private String userName;
    private String email;

    public Specification<BinsooUser> toSpecification() {
        return SpecBuilder.builder(BinsooUser.class)
                .addSpec(where(userNameLike(userName)))
                .addSpec(where(emailLike(email)))
                .toSpecification();
    }
}

컨트롤러 서비스 로직

더보기
package com.example.binsoojpa.controller;

import com.example.binsoojpa.domain.BinsooSearch;
import com.example.binsoojpa.domain.BinsooUser;
import com.example.binsoojpa.service.JpaService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class JpaController {

    private final JpaService service;

    @GetMapping("/user")
    public List<BinsooUser> findAll (BinsooSearch search){
        return service.findAll(search);
    }

    @GetMapping("/user/{id}")
    public BinsooUser findById(@PathVariable long id){
        return service.findById(id);
    }
}

 

package com.example.binsoojpa.service;

import com.example.binsoojpa.domain.BinsooSearch;
import com.example.binsoojpa.domain.BinsooUser;
import com.example.binsoojpa.repository.JpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class JpaService {

    private final JpaRepository repository;

    public List<BinsooUser> findAll (BinsooSearch search){
        return repository.findAll(search.toSpecification());
    }

    public BinsooUser findById (long id){
        return repository.findById(id);
    }
}

API 결과 테스트

더보기
http://localhost:8080/api/user?email=soo

[
    {
        "id": 1,
        "userName": null,
        "password": "1234",
        "email": "soo@nnn"
    }
]

console 에 아래와 같이 출력되는 것을 확인할 수 있다.

Hibernate: 
    select
        binsoouser0_.id as id1_0_,
        binsoouser0_.email as email2_0_,
        binsoouser0_.password as password3_0_,
        binsoouser0_.user_name as user_nam4_0_ 
    from
        binsoo_user binsoouser0_ 
    where
        1=1 
        and (
            binsoouser0_.email like ?
        )

JPA 의 검색 조건은 공통적인 부분을 어떻게 세워야 하는지가 큰 관건이라고 생각한다.

하지만 다양한 검색 조건을 경험해보지도 않고 간단한 API 개발만하고 JPA 를 한다는건

바다에 나가보지도 않은 사람이 바다속을 상상하는 꼴인 것 같다. 😒

 

JPA 를 내 주력 언어로 사용하기 위해서는 더 많은 경우의 수에 대처해봐야 할 필요가 분명히 있다.

그때까지 화이팅 .. 🔊


728x90
반응형