카카오 알림톡 프로젝트를 진행하면서 템플릿 관리 API
를 만들때 JPA Speicification
을 활용하여 기간 검색 조건과 키워드 조건을 묶어서 개발하게 되었다.
검색조건을 만들고, 기간과 키워드 검색을 묶어서 만드는 로직을 만들면서 배운것들이 많기에 TIL
포스팅을 하면서 정리하고자 한다.
기간별로 등록된 템플릿을 조회할 수 있도록 하기
기간별로 키워드 검색하여 등록된 템플릿을 조회할 수 있도록 하기
기간을 둘다 넣지 않을 경우에는 전체 기간을 디폴트로 조회할 수 있게 하기
페이지네이션해주기
페이지네이션시 최신 등록된 템플릿이 가장 위에서 조회될 수 있게 하기
NotificationController.java
@GetMapping("/templates") public ResponseDto<?> searchTemplate( NotificationSearchCriteria criteria, @PageableDefault(sort = "id", direction = DESC) Pageable pageable) { return new ResponseDto<>(service.searchEntityTemplate(criteria, pageable)); }
GetMapping
으로 지정해준다.
파라미터는 두개를 전달한다.
NotificationSearchCriteria
라는 DTO
클래스로 Request
를 받는다.
Pageable
인스턴스로 페이지네이션을 해준다.
@PageableDefault
어노테이션으로 간단하게 sorting
을 해줄 수 있다.
여기서는 id
를 기준으로 DESC
정렬을 해주었다.
NotificationSearchCriteria.java
@Getter @Setter @NoArgsConstructor public class NotificationSearchCriteria { private LocalDateTime startDate; private LocalDateTime endDate; private String keyword; public void setStartDate(String startDate) { this.startDate = startDateTime(toLocalDate(normalizer(startDate))); } public void setEndDate(String endDate) { this.endDate = endDateTime(toLocalDate(normalizer(endDate))); } }
컨트롤러를 통해 Client
의 request
을 받아내는 DTO
클래스다.
컨트롤러를 통해 들어오는 인자는 모두 String
값이므로, setter
를 override
하여 LocalDateTime
으로 형변환 시켜주었다.
참고로 normalizer, toLocalDate, startDateTime
은 회사에서 사용하는 util
클래스에 있는 static
메소드다.
이 클래스들은 매핑과 정규화를 시켜준다.
NotificationServiceImpl.java
@Override public TemplatePage searchEntityTemplate(NotificationSearchCriteria criteria, Pageable pageable) { return new TemplatePage(manager.searchTemplates(criteria, pageable)); }
service
계층은 manager
계층에서 만들어진 로직을 전달할 뿐이다.
여기서 중요한건 TemplatePage
객체로 반환한다는 것인데, 이 구조가 매우 중요하다.
manager.searchTemplates(criteria, pageable)
은 Page
타입을 리턴한다.
TemplatePage
는 Page
타입을 인자로 받아 한번 더 감싸주는 역할을 한다.
이는, RestDoc
을 만들 때 Pagination
부분을 간결화하여 문서작성을 용이하게하고, Client
에게 필요한 데이터만 전달해주기 위함이다. ⭐️
TemplatePage.java
public class TemplatePage extends ResponsePage<TemplateDto> { public TemplatePage(Page<TemplateDto> page) { super(page); } }
ResponsePage.java
@Data @NoArgsConstructor(access = AccessLevel.PROTECTED) public abstract class ResponsePage<T> { private List<T> content; private int page; private int size; private long total; public ResponsePage(Page<T> page) { this.content = page.getContent(); this.page = page.getNumber() + 1; this.size = page.getSize(); this.total = page.getTotalElements(); } }
TemplatePage
가 상속한 추상클래스다.
Pageable
인터페이스를 간결화하기 위함이다.
가공된 데이터는 content
에 담기고, pageable
필드들은 여기서 가공되어 리턴된다.
TemplateSpec.java
static Specification<NotificationTemplate> search(NotificationSearchCriteria criteria) { Specification<NotificationTemplate> spec = (root, query, builder) -> { List<Predicate> predicates = new ArrayList<>(); //startDate만 있을 경우 if (!isEmpty(criteria.getStartDate()) && isEmpty(criteria.getEndDate())) { predicates.add(builder.greaterThanOrEqualTo(root.get("createdAt"), criteria.getStartDate())); //endDate만 있을 경우 } else if (isEmpty(criteria.getStartDate()) && !isEmpty(criteria.getEndDate())) { predicates.add(builder.lessThanOrEqualTo(root.get("createdAt"), criteria.getEndDate())); //둘다 있을 경우 } else if (!isEmpty(criteria.getStartDate()) && !isEmpty(criteria.getEndDate())) { predicates.add(builder.between(root.get("createdAt"), criteria.getStartDate(), criteria.getEndDate() )); //둘다 없을 경우 } else if (isEmpty(criteria.getStartDate()) && isEmpty(criteria.getEndDate())) { predicates.add(builder.lessThanOrEqualTo(root.get("createdAt"), LocalDateTime.now())); } if (!isEmpty(criteria.getKeyword())) { predicates.add(builder.or( builder.like(root.get("subject"), "%" + criteria.getKeyword() + "%"), builder.like(root.get("content"), "%" + criteria.getKeyword() + "%") )); } return builder.and(predicates.toArray(new Predicate[0])); }; return spec; }
핵심 비지니스 로직이다. 두둥 ⚙️
해당 인터페이스의 위치는 관례상 Repository
에 위치한다.
왜냐하면 JpaSpecificationExecutor
를 implements
받은 Repository
와 같은 위치에 있는것이 자연스럽고, 의미상 맞기 때문이다.
greaterThanOrEqualTo
와 lessThanOrEqualTo
를 사용하면 이상, 이하가 연산이 되는데, 이는 날짜연산에도 동일하게 작동한다. specification만세
분기는 4가지로 처리했다.
startDate
만 있을 경우 -> start date
~ 오늘까지 조회endDate
만 있을 경우 -> ~ end date
조회between
을 사용하여 처리했다.keyword
조회를 더하였다.keyword
조회는 요구조건이 제목과 내용에서 조건을 조회해달라고 하였으므로, like
를 사용하였고,or
로 묶었다. TasonTemplateRepository.java
public interface TasonTemplateRepository extends JpaRepository<NotificationTemplate, Long>, JpaSpecificationExecutor<NotificationTemplate> { Optional<NotificationTemplate> findByTemplateCode(String templateCode); @Query("select count(*) from NotificationTemplate n where n.templateCode = :code") int checkDuplicateCode(@Param("code") String code); @Override Page<NotificationTemplate> findAll(Specification<NotificationTemplate> spec, Pageable pageable); int deleteByTemplateCode(String templateCode); }
참고로 Repository
는 interface
로 구현하는것이 좋다.
인터페이스로 구현 후, 인터페이스끼리는 다중상속이 되므로, JpaRepository
와 JpaSpecificationExecutor
를 상속받았다.
checkDuplicationCode
메소드는 jpql
을 사용하여 중복체크를 하기위해 사용하였다.
이번 프로젝트를 통해서 배우는 것이 참 많다.
Specification
과 Pagination
을 JPA
를 통해 하면서 느끼는 것은 JPA
가 정말 혁신적이다라는 것이다. 검색조건과 페이징을 하기위해 sql
노가다를 하며 끙끙거리던 예전의 내모습이 스쳐간다.
이 포스팅을 읽는 분들이 해당 예제를 통해 JPA
의 specification
이 조금이라도 더 잘 이해가 되셨길 바래본다.
그럼 이만 ! ✋