프로그래밍

[웹 프로그래밍] 스프링부트JPA 7-2. QueryDSL설명~. 검색기능.

tt2t2am1118 2023. 1. 12. 01:27
반응형

안녕하세요. CRUD를 생성해서, 게시판 데이터 형태로 만들었습니다.

그런데, 중복코드가 또 많더라구요. 자동화코드를 만들예정입니다.

이번에는 검색. QueryDsl의 주요기능인 검색기능을 활용했습니다.

한 데이터, 엔티티 테이블에, 검색이 많은 경우. 로직을 작성하기 어렵다. 이렇게 이야기할 수도 있죠. 하지만, QueryDSL로 검색기능을 구현하면 비교적 쉽게 구현할 수 있습니다.

예를 들어,

사용자의 가입일이 2023년 1월 12일인 경우. 그리고 메일이 abc@mail.com 인 경우. 가입일이 1월 1일 부터 1월 12일까지 이면서, 메일에 abc가 들어가는 경우를 검색해달라. 이런 경우가 있겠죠~.

이러한 Query의 검색을 쉽게 만들 수 있습니다. 검색조건이 되는 것은 Condition 클래스에 담아줌니다. 입력을 받는 부분에는, 텍스트 필드, 날짜의 시작일, 끝일, 또 어떤 부분을 검색해야 할까... 이 부분을 담은 select 태그. 그리고 검색버튼을 달아주면 되겠죠.

각 엔티티 마다, 이런 Condition클래스를 다 만들어 주면 되겠죠~.


위와 같은 검색박스 형태가 사용될때에, QueryDSL이 사용된다고 생각하시면 될검니다~.

 

 편한점은, 굳이 로직구현이 있지 않아도, 빈칸이면... 그냥 전체가 나오고, 시작일만 있으면, 시작일만 포함하구요. 예를 들어, id를 선택하고, 키워드에 숫자를 적고. 그렇게만 검색도 되고, 날짜도 넣어서 기간을 추가해서도 검색이 되구요. 날짜 부분에 Date html을 달아서 테스트 해봐야겠네요. 키워드는 현재 되는군요.

 

 


실제 위의 html의 내용을 보면... 타임리프로 되어있이요. condition이라는 th:object를 가져와서, 검색 버튼. submit버튼이죠. 그 버튼을 누르면, 리스트를 다시 호출해서, 값을 보여주죠~. form태그로, get매핑.

Github에 올렸습니다. 한번 테스트해보세요.
https://github.com/infott2t/springboot-querydsl-ex

 

GitHub - infott2t/springboot-querydsl-ex: SpringBoot JPA + QueryDSL, First Instance Project

SpringBoot JPA + QueryDSL, First Instance Project. Contribute to infott2t/springboot-querydsl-ex development by creating an account on GitHub.

github.com


값은 몇개가 있을까요? 그렇죠. id, 주소. 이 셀렉트 태그 값, 키워드명, 텍스트 필드값. 시작일, 종료일도 전송이 되는것이죠.

그렇게 값만 넘겨주면 됨니다. 그런데, 위처럼 타임리프. th:object=${변수명}. 이렇게 사용하려면, 지금 위 페이지를 불러오는 컨트롤러 메소드에서 new Condition(). 이렇게 처럼, 해당 빈 컨디션을 보내줘야하는 것이 있죠. model.addAttribute("condtion" , condition); 이렇게 보내면, 뷰페이지에서 th:object="${condition}" 이렇게 받을 수 있게 되는 것이죠.

또, 서브밋 이후, 전달 받는 값에도 condition을 전달받아야 하니, 메소드의 인자값이 될테구요. 그렇습니다.

@Controller
public class InstanceUrlRoleUSERController{

...

@GetMapping("/administer/instanceurl/roleclass/user")
public String index(Model model, RoleUSERSearchCondition condition) {

    List<RoleUSERApiDto> lists = roleUSERService.searchFindAllDESC();
    
    model.addAttribute("lists", lists);
    model.addAttribute("condition", condition);
    
    return "firstinstance/roleclass/user/index"
}

}


위에 코드를 보면, 설명한 대로, condition을 인자값으로 받고... -> 이의 경우, 뷰 페이지에서 넘어오는 Get parameter가 들어있게 되겠죠.
또, model.addAttribute에 condition을 담으면, 실제 뷰페이지에서, th:object에 사용할 수 있게 되는 것이구요. 그럼 이번엔 뷰 페이지를 볼까요.

index.html.

<form name="search_form" th:action="@{/administer/instanceurl/roleclass/user}" method="get" role="form" th:object="${condition}" class="d-flex justify-content-evenly">
    <table style="width:670px;" class="border border-5 d-flex justify-content-center caption-top">
      <colgroup>
        <col style="width:10%;">
        <col style="width:35%;">
        <col style="width:10%;">
        <col style="width:35%;">
        <col style="width:auto;">
        <col style="width:auto;">
      </colgroup>
      <tbody>
      <tr>
        <th class="font-12">키워드</th>
        <td class="font-12">
          <select id="field" name="field" style="width:60px;" title="키워드 선택">
            <option th:value="id" th:selected="${#strings.trim(param.field) eq 'addressStr.id'}">id</option>
            <option th:value="address" th:selected="${#strings.trim(param.field) eq 'addressStr.addrFull'}" selected>주소</option>
          </select>
          <input class="font-12" type="text" title="키워드" placeholder="키워드명 입력" name="s" th:field="*{s}" autocomplete="on"  style="vertical-align: top; width:100px;">
        </td>
        <th scope="row" class="font-12">&nbsp;등록일자</th>
        <td class="font-12">
          <input type="text" placeholder="시작일" class="ico_date" name="sdate"
                 id="datepicker1" th:field="*{sdate}" autocomplete="on" style="width:100px;">
          <span class="hypen">~</span>
          <input type="text" placeholder="종료일" class="ico_date" name="edate"
                 id="datepicker2" th:field="*{edate}" autocomplete="on" style="width:100px;">
        </td>
        <td>
          &nbsp;<button class="btn btn-success btn-sm">검색</button>
        </td>
        <td>
          &nbsp;<a class="btn btn-sm btn-primary" th:href="@{/administer/instanceurl/roleclass/user/insert}">쓰기</a>&nbsp;
        </td>
      </tr>
      </tbody>
    </table>

  </form>

잘 보시면 알 수 있듯이, form으로 감싸서 검색 버튼에 submit을 단 것이구요. form태그에 th:object에 condition이 들어있죠. 바로전에 설명한 해당 컨트롤러 메소드에서 넘긴 파라메터. addAttribute의 이름이 적힌 것이구요. 그리고, 나머지 보시면, *{}이런 형태. 이것은 바로, condition이 가진 변수들이 적힌 것이라고 생각하면 됨니다. condition변수는, 컨트롤러에서 적었듯이 RoleUSERSearchCondition형이구요.

s와 sdate, edate를 변수로 가지고 있는 것이죠. 실제, 곱하기 기호를 빼고, th:field="${condition.s}" 이런 식으로 적어도 잘 작동합니다.
곱하기가 적혀있을때는, 실제 자기자신의 이름을 안적었구나. 이렇게 생각하면 되겠죠.

@Data
public class RoleUSERSearchCondition {

    private String field;       //셀렉트 태그 값. id와 address
    private String s;          // 텍스트 필드값

    private String sdate;      //시작일
    private String edate;      //종료일 
}

위처럼 되어있는 것입니다. 그럼, 이 컨디션은 RoleUSERService에서 가져가서 사용하는 것이 되구요~.


서비스 클래스는, 컨트롤러 클래스에서 사용한다고 생각하면 쉽게 생각할 수 있죠.

컨트롤러 클래스. 어떤 메소드에서 파라메터를 가지고 와서, 어떤 엔티티 서비스 클래스에 집어넣고, 값을 리스트에 담아서, 출력. 이런 형태.

엔티티마다, 서비스가 하나씩은 있고, 리파지토리도 하나씩 있죠. 리파지토리가 실제로, save. 저장. 또, findById. 아이디 검색. 그리고, 지금하게되는 search로 시작하는 검색. 이 부분이 QueryDSL이 사용되는 부분이구요.

실제 엔티티의 서비스 클래스에서는 별 특별한 것은 없습니다. 똑같죠.

package org.example.domain.roleclass.user;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class RoleUSERService {

    private final RoleUSERRepository roleUSERRepository;
    

    @Transactional(readOnly = true)
    public RoleUSER findById(Long id) {
        return roleUSERRepository.findById(id).orElseThrow();
    }

    @Transactional
    public void save(RoleUSER roleUSER) {
        roleUSERRepository.save(roleUSER);
    }
    
    
    @Transactional(readOnly = true)
    public List<RoleUSERApiDto> searchFindAllDesc() {
        return  roleUSERRepository.searchFindAllDesc();
    }

    @Transactional(readOnly = true)
    public Page<RoleUSERApiDto> searchAllV2(RoleUSERSearchCondition condition, Pageable pageable) {
        return roleUSERRepository.searchAllV2(condition, pageable);
    }
}

위에 코드를 보면, 마찬가지로, findById, save, searchFindAllDesc(), searchAllV2()이런 메소드이름이 있구요. findById와 save는 익숙한 메소드이름이죠.

findById와 save. 실제 id로 검색할때. 데이터를 저장할 때 사용하는 것이구요. 또, 잘 보시면, findById의 리턴 값에 orElseThrow()라는 이름이 붙어 있습니다. 실제 값이 없는 경우, 예외가 발생하니, 꼭 findById로 검색하는 경우, 해당 서비스를 사용하는 컨트롤러에서, try-catch문을 감싸주세요. 인텔리제이 Github Copliot을 사용하면 자동화 코드를 만들어주는데... 자동으로 만들라고 코드로 표시하기도 하구요.

또 서비스 클래스에는 @Transactional이라는 어노테이션을 메소드마다 달아줘야합니다. 저장되는 경우는 @Transactional. 읽기. 리스트 불러오기 같은 작업인 경우는, @Transactional(readOnly=true) 이런 식으로 달아줘야하죠. 안달면, 오류 발생하죠.


QueryDSL을 사용하는 경우에는, search라는 이름을 앞에 주어서 일반 메소드와 구분을 지어줌니다. searchFindAllDesc의 경우에는 리스트 전체 출력. searchAllV2의 경우에는 페이징과 검색조건을 넣은 형태이죠.

그래서,

컨트롤러에서, 엔티티의 서비스를 적어서 데이터를 가져오게 된다. 이부분...

엔티티는 서비스 클래스, 리파지토리가 하나씩 있다.

서비스 클래스에는 findById, save 메소드가 있다. QueryDSL을 사용하는 경우, 메소드 이름에 search를 붙여주자. 이렇게 요약할 수 있을 검니다.

일반적으로는 데이터 테이블 하나에, 리파지토리 1개, 서비스 클래스 1개, 엔티티 클래스 1개, 데이터를 주고 받고 하는 Dto클래스. 이렇게만 구성되지만...

QueryDSL의 경우에는 검색값. Condition과 연결해야하는 작업이 있기 때문에, 리파지토리 클래스. 인터페이스죠. 리파지토리 인터페이스와 함께, RepoditoryCustom 이라는 인터페이스. 그다음 Repositoryimpl 이라는 클래스가 사용됨니다.

뭔가 복잡할 것 같지만, 인터페이스가 그렇듯이... RepositoryCustom 인터페이스에는 그냥 메소드 이름만 적어주고, 실제로 내용은 RepositoryImpl 클래스에 다 적게 되죠.

그래서, condition. 검색조건과 페이징을 같이 적어서 메소드로 실제로는 RepositoryImpl 클래스에 적어놓게 되구요. 리턴값은, 서비스 클래스에 바로 나오는 것이죠.

인터페이스 상속이 일어나기 때문에, 그냥 Custom에 한번 적고, Impl에 내용이 적히고... 이렇습니다.

강의 를 들어보시거나, 한번정도 만들어 보시면, 쉽게 알 수 있습니다.

리파지토리가 이렇게 바뀌게 되죠.

@Repository
public interface RoleUSERRepository extends JpaRepository<RoleUSER, Long>,
        QuerydslPredicateExecutor<RoleUSER>, RoleUSERRepositoryCustom {


}

RoleUSER 엔티티의 레파지토리 입니다. QuerydslPredicateExecuter... 이 부분은 한번 적어주고, RoleUSERRepositoryCustom. 리파지토리 Custom과 연결해주고요. extends하고 있죠. 위처럼만 적어주면 끝이나고... 그다음, 레파지토리Custom역시, 간단합니다.

public interface RoleUSERRepositoryCustom {

//페이징, 검색조건을 붙인 형태로 검색. 검색 결과 값은 Page형이지만 리스트와 거의 같다. 페이징 변수들이 포함되어서 페이지 네비게이션을 만들 수 있게 된다.
    Page<RoleUSERApiDto> searchAllV2(RoleUSERSearchCondition condition, Pageable pageable);

//전부 검색할때. 페이징과 검색조건이 없다. 리스트형 반환.
  List<RoleUSERApiDto> searchFindAllDesc();


}

이렇게가 끝이예요. 아까 설명했듯이, 그냥 이름만 적히는 것이죠. 실제 구현은 Impl에서 하는 것이죠. 레파지토리가 3개가 되는 것이죠.
그냥 리파지토리, 리파지토리Custom, 리파지토리Impl

할일은,

리파지토리에, 확장이름을 한번 적어준다. extends 리파지토리Custom. 그리고, QuerydslPredicateExecutor<RoleUSER>이 부분도 적구요.

리파지토리Custom에는 사용될 메소드들을 이름만 적어줌니다. 그다음, 리파지토리Impl을 만들어서, 실제 내용을 적어서, 리턴. 리스트 리턴, Page리턴이겠죠. 그렇게 해주는 것이죠.

public class RoleUSERRepositoryImpl implements RoleUSERRepositoryCustom {

//적어줘야하는 부분.
    private final JPAQueryFactory queryFactory;
//적어줘야하는 부분.
    public RoleUSERRepositoryImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

//메소드에 실제 내용을 적고, 리턴해준다.

 @Override
    public List<RoleUSERApiDto> searchFindAllDesc() {
        List<RoleUSERApiDto> content = queryFactory.
                select(Projections.constructor(RoleUSERApiDto.class,
                      roleUSER.id,
                        roleUSER.id,
                        roleUSER.addressStr,
                        roleUSER.phoneStr,
                        roleUSER.createdDate,
                        roleUSER.modifiedDate
                )).from(roleUSER).where(roleUSER.isDel.eq("N"))
                .leftJoin(roleUSER.addressStr, addressStr)
                .leftJoin(roleUSER.phoneStr, phoneStr)
                .orderBy(roleUSER.id.asc())
                .fetch();


        return content;
    }
}

searchFindAllDesc메소드의 내용입니다. 리파지토리Impl에서 실제 구현한 것이죠~. select문으로 불러서, 값을 List형으로 담아서 넘겼죠~.

이런 식입니다. 내용이 길어졌네요. 여기까지입니다. 처음부터, 다 해봐야지... 이런 것보다, 그냥 그렇구나. 이렇게 생각해도 좋구요~.

한개의 엔티티를 생각해서, QueryDSL 실제 구현해본다면, 다음에는 쉽게 만들 수 있을 검니다. 제가 적어놓은 글, 1번에 엔티티 만드는 자동화코드. 스윙으로 만들어서, 공부해보는 것을 추천해보네요.

[웹 프로그래밍] 스프링부트JPA 1. 데이터를 클래스화 하기. 자동 생성. (tistory.com)

 

[웹 프로그래밍] 스프링부트JPA 1. 데이터를 클래스화 하기. 자동 생성.

공부해봅시다. 오늘은 클래스 형식으로 데이터 만들어보기 입니다. 스프링부트를 통해서, 엔티티클래스를 만들면, 자동화를 통해 프로그래밍을 더 쉽게 할 수 있습니다. 백엔드와 프론트엔드로

tt2t2am.tistory.com


저도 공부하면서, 언제하지 어떤 막막함이있었는데요. 저의 경우에는, url이름을 적는다던가, 코드 적으면서, 중복되는 코드 적기... 되풀이 되는 코드니까요. 그래서 뭔가 긴장되고 버벅이게 되고 그렇죠. 스트레스이죠. 하지만, 완성되고 나서, 보니 이해도 되고, 성취감도 있네요.

다음 시간에는, 중복코드를 제거하는 그런 코드를 만들 예정입니다. 버벅이지 않아도 되겠죠. 포멧형태를 만드는 작업이라 한번정도 CRUD는 만들어 놓아야, 중복되는 코드를 제거 할 수 있겠죠. 뷰페이지. First instance. 지금처럼 또 프로젝트를 진행할때, 데이터 가공. 이렇게 서비스를 만들어나갈 때 효율을 높일 수 있을거라고 생각하네요.

좋은 하루되세요.

--
저의 글, 봐 주셔서 감사합니다.


반응형