TIL (20210820) - JPA Specification 으로 기간별 검색조건 만들기

joshuara7235·2021년 8월 20일
1

📖 TIL

목록 보기
6/8
post-thumbnail

🌱 서론

  • 카카오 알림톡 프로젝트를 진행하면서 템플릿 관리 API를 만들때 JPA Speicification을 활용하여 기간 검색 조건과 키워드 조건을 묶어서 개발하게 되었다.

  • 검색조건을 만들고, 기간과 키워드 검색을 묶어서 만드는 로직을 만들면서 배운것들이 많기에 TIL 포스팅을 하면서 정리하고자 한다.

👉 요구조건

  • 기간별로 등록된 템플릿을 조회할 수 있도록 하기

  • 기간별로 키워드 검색하여 등록된 템플릿을 조회할 수 있도록 하기

  • 기간을 둘다 넣지 않을 경우에는 전체 기간을 디폴트로 조회할 수 있게 하기

  • 페이지네이션해주기

  • 페이지네이션시 최신 등록된 템플릿이 가장 위에서 조회될 수 있게 하기

⚙️ 자, 그럼 개발해볼까?

🗝 Rest Controller

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정렬을 해주었다.

🗝 Request DTO

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)));
    }
}
  • 컨트롤러를 통해 Clientrequest을 받아내는 DTO 클래스다.

  • 컨트롤러를 통해 들어오는 인자는 모두 String값이므로, setteroverride하여 LocalDateTime으로 형변환 시켜주었다.

  • 참고로 normalizer, toLocalDate, startDateTime은 회사에서 사용하는 util클래스에 있는 static 메소드다.
    이 클래스들은 매핑과 정규화를 시켜준다.

🗝 Service 와 TemplatePage DTO

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타입을 리턴한다.
    TemplatePagePage타입을 인자로 받아 한번 더 감싸주는 역할을 한다.
    이는, 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 필드들은 여기서 가공되어 리턴된다.

🗝 Business Logic

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에 위치한다.
    왜냐하면 JpaSpecificationExecutorimplements 받은 Repository 와 같은 위치에 있는것이 자연스럽고, 의미상 맞기 때문이다.

  • greaterThanOrEqualTolessThanOrEqualTo를 사용하면 이상, 이하가 연산이 되는데, 이는 날짜연산에도 동일하게 작동한다. specification만세

  • 분기는 4가지로 처리했다.

  1. startDate만 있을 경우 -> start date ~ 오늘까지 조회
  2. endDate만 있을 경우 -> ~ end date 조회
  3. 둘다 있을 경우 -> 해당 기간 조회
    • 이 경우 between을 사용하여 처리했다.
  4. 둘다 없을 경우 -> 오늘을 기준으로 전에 등록한 모든 템플릿 조회
  • 기간이 조회된 후에 keyword조회를 더하였다.
    - keyword조회는 요구조건이 제목과 내용에서 조건을 조회해달라고 하였으므로, like를 사용하였고,
    두개의 검색조건을 or로 묶었다.

✋ Repository

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);
}
  • 참고로 Repositoryinterface로 구현하는것이 좋다.

  • 인터페이스로 구현 후, 인터페이스끼리는 다중상속이 되므로, JpaRepositoryJpaSpecificationExecutor를 상속받았다.

  • checkDuplicationCode메소드는 jpql을 사용하여 중복체크를 하기위해 사용하였다.

🙏 오늘의 TIL을 마치며

  • 이번 프로젝트를 통해서 배우는 것이 참 많다.

  • SpecificationPaginationJPA를 통해 하면서 느끼는 것은 JPA가 정말 혁신적이다라는 것이다. 검색조건과 페이징을 하기위해 sql 노가다를 하며 끙끙거리던 예전의 내모습이 스쳐간다.

  • 이 포스팅을 읽는 분들이 해당 예제를 통해 JPAspecification이 조금이라도 더 잘 이해가 되셨길 바래본다.

  • 그럼 이만 ! ✋

profile
인문학 하는 개발자 💻

0개의 댓글