JPA와 QueryDSL로 SQL한줄도 없이 JSON API 만들기

Brant Hwang
QueryPie
Published in
10 min readNov 11, 2016

부제: 내가 이러려고 이때까지 SQL 작성했나.. 자괴감 들고 괴로워.. (ㅋㅋ)

지난번 포스팅에서는 AXBoot Initialzr와 AXBoot 개발자 도구를 활용해 빠르고 간단하게 JSON API를 만들었습니다. 하지만 실제로 서비스에 사용할 API라 하기에는 조금 부족하죠. 예를 들면 다음과 같은 조건이 있을 것 같습니다.

  • 특정 ID를 가진 고객만 검색
  • 특정 전화번호, 주소, 이름으로 고객을 검색
  • 전화번호, 주소, 이름 전체에서 검색

API에 매개변수를 전달하고, 해당 매개변수에 따라 동적으로 데이터를 조회할 수 있는 기능을 만들어보겠습니다. (하지만 단 한 줄의 SQL도 작성하지 않을 겁니다!)

1. CustomerService를 다음과 같이 구성합니다.

public List<Customer> gets(RequestParams<Customer> requestParams) {
int id = requestParams.getInt("id", -1);
String phone = requestParams.getString("phone", "");
String addr = requestParams.getString("addr", "");
String name = requestParams.getString("name", "");
String filter = requestParams.getString("filter", "");
QCustomer customer = QCustomer.customer;
BooleanBuilder builder = new BooleanBuilder();
if (id != -1) {
builder.and(customer.id.eq(id));
}
// 필터 매개변수가 있을 경우 Phone, Address, Name에서 Like 검색.
if (isNotEmpty(filter)) {
builder.and(
customer.customerPhone.like(like(phone))
.or(customer.customerAddr.like(like(addr)))
.or(customer.customerName.like(like(name))));
} else {
// 필터 매개변수가 없으면 각 매개변수가 있는지 확인하고, 전달된 매개변수가 있으면 검색조건에 추가.
if (isNotEmpty(phone)) {
builder.and(customer.customerPhone.like(like(phone)));
}
if (isNotEmpty(addr)) {
builder.and(customer.customerPhone.like(like(phone)));
}
if (isNotEmpty(name)) {
builder.and(customer.customerPhone.like(like(phone)));
}
}
return select()
.from(customer)
.where(builder)
.fetch();
}
AXBoot에는 RequestParams 라는 요청 매개변수 유틸리티가 있는데요, RequestParams를 사용하면 QueryString에 전달된 매개변수를 간단하게 가져올 수 있습니다. (제공된 매개변수가 없을 경우, 두 번째 인자로 전달한 기본값을 반환합니다)Q로 시작하는 QueryDSL Customer 클래스와, 매개변수 조건에 따라서 동적으로 WHERE 절을 추가하는 BooleanBuilder를 선언했습니다.매개변수 제공 여부에 따라 BooleanBuilder에 조건을 추가해주고, 마치~ 쿼리처럼 생긴 Java 코드 몇 줄만 작성하면, 동적 쿼리가 수행됩니다. (정말이겠죠?)테스트를 위해 개발자 도구의 Swagger로 가서, GET /api/v1/customer를 열어봅니다.
screen-shot-2016-11-11-at-2-35-48-pm
하지만, 매개변수를 전달할 방법이 없는 것 같네요. 물론 브라우저에 URL을 붙여넣고, QueryString으로 전달하면 되겠지만, API를 설계한 개발자 외 또 다른 개발자가 해당 API를 테스트해보기에는 적합한 방법이 아닌 것 같습니다.이제 Swagger에서 매개변수를 전달할 수 있도록 몇 가지 애너테이션을 추가해보겠습니다.2. Controller에 ApiImplicitParams 추가하기AXBoot 개발자 도구에서 제공하는 Swagger는 스프링 컨트롤러에 @RequestParam 애너테이션이 있으면, 자동으로 매개변수를 전달할 수 있는 UI를 생성해줍니다. 하지만 AXBoot에서는 @RequestParam 대신 RequestParams라는 클래스로 매개변수를 추상화 하므로, 명시적인 Swagger 매개변수 애너테이션을 선언해주어야 합니다.다음과 같이 GET 메서드에 애너테이션을 추가합니다.@RequestMapping(method = RequestMethod.GET, produces = APPLICATION_JSON)
@ApiImplicitParams({
@ApiImplicitParam(name = "filter", value = "전체 고객데이터 검색을 위한 매개변수", dataType = "String", paramType = "query", required = false),
@ApiImplicitParam(name = "phone", value = "전화번호 검색 매개변수", dataType = "String", paramType = "query", required = false),
@ApiImplicitParam(name = "addr", value = "주소 검색 매개변수", dataType = "String", paramType = "query", required = false),
@ApiImplicitParam(name = "name", value = "이름검색 매개변수", dataType = "String", paramType = "query", required = false),
@ApiImplicitParam(name = "id", value = "ID", dataType = "Integer", paramType = "query", required = false)
})
public Responses.ListResponse list(RequestParams<Customer> requestParams) {
List<Customer> list = customerService.gets(requestParams);
return Responses.ListResponse.of(list);
}
서버를 재시작한 후, Swagger에 다시 가보면 다음과 같이 매개변수를 전달할 수 있는 UI가 생성됩니다.
screen-shot-2016-11-11-at-2-46-43-pm
이제 매개변수 전달을 통해서 조건에 맞는 JSON 응답이 오는지 확인해보겠습니다.filter, phone, addr, name 등에 값을 입력하고 Try it out을 해보면, 다음과 같이 조건에 맞는 쿼리가 동적으로 생성되어 데이터베이스에 질의 됩니다.저는 전화번호에 3465를 입력한 후 조회를 해보았더니 다음과 같은 쿼리와 JSON 응답이 왔습니다.
screen-shot-2016-11-11-at-2-52-10-pm
screen-shot-2016-11-11-at-2-52-20-pm
phone 뿐만아니라, 조건에 따라서 쿼리가 동적으로 생성되기 때문에, Raw SQL을 작성하는것 보다 타입 안정성과 가독성이 뛰어난 쿼리를 생성할 수 있습니다.이제 특정 id를 가진 데이터를 조회해봅니다.
screen-shot-2016-11-11-at-4-16-35-pm
id에 일치되는 데이터가 나왔지만, 클라이언트 개발자는 API 주소와 JSON 응답이 마음에 들지 않는다며, 다음과 같이 변경 요청을 했습니다.주소는 /api/v1/customers/{customer_id} 형태, JSON은 다음과 같다고 해보겠습니다.{
"id": 2,
"customerName": "장기영",
"customerPhone": "010-8881-9136",
"customerAddr": "경기도 포천시 송우리",
"customerEmail": "tom@axisj.com",
"dataStatus": "ORIGIN",
"__deleted__": false,
"__created__": false,
"__modified__": false
}
Controller와 Service에 새로운 메서드를 추가해서 /api/v1/customers/{customer_id} 형태의 API를 추가합니다.@RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = APPLICATION_JSON)
public Customer get(@PathVariable Integer id) {
return customerService.get(id);
}
Service는 SpringDataJPA가 제공하는 findOne 메서드를 사용해서 다음과 같이 작성했습니다.public Customer get(Integer id) {
return findOne(id);
}
참~ 쉽죠잉. QueryDSL을 사용해서 다음과 같이 작성할 수도 있습니다.public Customer get(Integer id) {
QCustomer customer = QCustomer.customer;
return select()
.from(customer)
.where(customer.id.eq(id))
.fetchOne();
}
많은 설정을 AXBoot가 미리 구성해놓았기 때문에, 조금 더 편하고 빠르게 JPA와 QueryDSL을 사용할 수 있습니다. 하지만 AXBoot 또한 근본적으로는 SpringDataJPA, Hibernate, QueryDSL 표준을 기반으로 구성되어 있기때문에, Core와 AXBoot 설정을 변경해서 입맛대로 구성을 바꿀 수도 있습니다.물론 JPA와 QueryDSL이 완전한 RawSQL(SQL Mapper)를 대체할 수는 없지만, 객체관계를 기반으로 JPA 구조에 적합한 테이블 구조가 수반되면 안전하고 유지 보수성이 뛰어난 코드를 작성할 수 있습니다.다음 편에서는 JsonView를 활용해 원하는 JSON 필드만 직렬화하는 방법을 소개합니다.도움이 되셨다면 Github Star 꼭 부탁드려요Star
Star

--

--