개발 일지/주간 개발 일지

[04월 1주차] 페이징(Paging), QueryDSL, LocalDate와 String 변환 방법

계란💕 2023. 4. 8. 23:18

Query DSL

 

  Ex)  여러 개의 검색 필드(모델 ID, 모델명, 등)를 입력 받아서 해당 조건을 만족하는 엔티티를 불러오려고 한다. 

  • 그런데 검색의 특성상 콤보 박스에서 선택(null) 상태로 두고 검색 버튼을 누르는 경우가 있다. 
  • 그런 경우에는 해당 조건에 상관없이 모든 데이터를 보여주려고 한다. 
  • 예를 들어, 검색 필드 중에서 "모델 유형" 필드가 있을 때, 여기를 비운 상태로 검색을 누르면 모든 모델 유형에 대한 데이터를 가져오려고한다. 
<hide/>
@Generated("com.querydsl.codegen.EntitySerializer")
public class QModel extends EntityPathBase<Model> {

    private static final long serialVersionUID = 1612220557L;
    public static final QModel model = new QModel("model");
    public final StringPath modelId = createString("modelId");
    public final StringPath modelNm = createString("modelNm");
   	...
    
    public QModel(String variable) {
        super(Model.class, forVariable(variable));
    }

    ..
}

 

  참고)
https://github.com/rbsks/WorkDuo_dev/blob/master/src/main/java/com/workduo/group/gropcontent/repository/query/impl/GroupContentQueryRepositoryImpl.java
https://sas-study.tistory.com/393

 

  • ModelQuery 클래스를 만들어서 쿼리 역할을 하는 메서드를 작성한다.
    •  다음과 같이 BooleanBuilder를 이용했다. 
    • BooleanBuilder: 쿼리의 WHERE 절 조건을 만들어주는 클래스이다. 그런데 가독성이 떨어지기 때문에 BooleanExpression으로 바꿀 계획이다. 
    • and()를 걸어주면서  WHERE 조건에 맞춰서 내용을 추가한다. 
    • predicate() => WHERE 절 역할
    • projection() => SELECT 역할
    • StringUtil 클래스는 회사에서 직접 만든 클래스이다. 
      • isNotEmpty(): isEmpty() 메서드와 반대이다. 
      • isEmpty()는 매개변수가 null이거나 "" 인 경우 true를 반환하는 메서드이다. 
    • offset, limit는 페이징을 위해 필요한 변수이다. offset은 시작 인덱스값, limit은 보여줄 데이터 개수를 의미한다. 따라서 limit는 1이상의 값이 들어가야 데이터를 보여줄 수 있다.  
<hide/>

@Component
@RequiredArgsConstructor
public class ModelQuery {

    private final JPAQueryFactory queryFactory;
    private final QModel model = QModel.model;

    // WHERE
    public BooleanBuilder predicate(Model modelVo) {

        BooleanBuilder search = new BooleanBuilder();
        if (StringUtil.isNotEmpty(modelVo.getModelId())) {
            search.and(model.modelId.eq(modelVo.getModelId()));
        }
    	..
        return search;
    }
    
     // SELECT
    private QBean<Model> projection() {
        return Projections.bean(Model.class,
                model.modelId,
                model.modelNm,
                ...;
    }
    
    public List<Model> getListByPaging(Model modelVO) {
        int offset, limit;
        List<Model> resultList;
        offset = (modelVO.getCurPage() - 1) * modelVO.getPageListSize();
        limit = modelVO.getPageListSize();
        resultList = queryFactory.select((projection()))
                .from(modelVO)
                .where(predicate(modelVO))
                .orderBy(modelVO.modelId.asc())
                .offset(offset)
                .limit(limit)
                .fetch();
        return resultList;
    }
}

 


<hide/>
@Configuration
public class JPAConfig {

	@PersistenceContext
	private EntityManager entityManager;

	@Bean
	public JPAQueryFactory jpaQueryFactory() {
		return new JPAQueryFactory(entityManager);
	}
}

JPA 메서드 명명 규칙

  • findByIdAndName(id, name): id, name 모두 일치하는 데이터만 가져온다. 
  • findByIdOrName(id, name): id나 name이 일치하는 데이터만 가져온다. 
    • 여러 조건 중  몇 개가 null 인 경우도 포함인지 null은 아니어야만 하는지 모르겠다.
  • findByNameIsNull(): Name 필드가 null 인 데이터를 가져온다. 

 

  • 검색 조건을 위해서 다음과 같은 쿼리를 짰지만
  • 아래 쿼리로 JPA 메서드 관련된 오류가 발생해서 QueryDSL로 방향을 바꿨다. 
<hide/>

@Query(
    " SELECT m FROM Model m " +
    " WHERE (:modelId is null OR m.modelId = :modelId) "
  + " AND (:modelNm is null OR m.modelNm = :modelNm) "
  + " AND (:modelCd is null OR m.modelCd = :modelCd) "
  + " AND (:modelExeCd is null OR m.modelExeCd = :modelExeCd) "
  + " AND (:useYn is null OR m.useYn = :useYn) "
)

 

  참고) https://zara49.tistory.com/130


HttpStatus code

  • 200 - 정상
    • 200: OK
    • 201: CREATED(서버에 어떤 자원이 생성됐을 때)
    • 204: NO_CONTENT(DELETE 요청 성공)
  • 300: redirect URL  오류
    • 예를 들어, OAuth 로그인처럼 어떤 URL에 연결하는 경우 연결된 주소에 이상이 있는 경우 발생한다. 
  • 400 - 클라이언트 오류
    • 400: BAD_REQUEST
    • 404: NOT_FOUND
    • 409: CONFLICT (생성 실패 또는 수정 실패)
  • 500 - 서버 오류

Java  Serializable 인터페이스

  • 객체를 직렬화할 수 있다. (객체를 바이트 스트림(byte stream)으로 변환)
  • 객체를 파일이나 네트워크를 통해 전송하거나 저장 가능
  • Serializable 인터페이스를 implements한 클래스는 ObjectOutputStream 클래스를 사용해서 객체를 직렬화할 수 있다.
  • 직렬화된 객체는 ObjectOutputStream를 사용해서 파일이나 네트워크에 전송하거나 ObjectOutputStream 클래스를 사용해서 다시 역직렬화해서 객체로 복원 가능하다. 

Paging(페이징)

  • 페이지 별로 목록 조회하기
  • Model를 매개변수로 넣어서
  •  Map<String, Object> 형태로 ("data", ModelList 정보), ("page", page 정보)를 넣어준다. 
  • 아래와 같이 commons 디펜던시를 추가한다. 
    • API의 기능 확장을 목표로 하는 라이브러리이다. 
    • 개발자들이 자주 사용하는 기능(문자열 처리, 배열 및 숫자 조작, 동시성 등 여러 순서가 지정된 데이터 구조 구현 등)을 제공
    • 아래에서 ToStringBuilder 클래스를 사용하기 위해 추가했다. 
<hide/>
<dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-lang3</artifactId>
</dependency>

 

 

  • BaseUtil
    • ToStringstyle 클래스를 이용해서 문자열을 형식화할 수 있다. 필드의 출력 형식, 문자열의 시작과 끝에 사용되는 특수 문자, 필드값이 null일 때,  출력되는 문자열 등을 지정 가능하다. 다양한 출력 스타일을 제공한다. 
    • ToStringBuilder: 객체를 문자열로 표현할 때 사용된다.
    • reflectionToString()은 반복적으로 append()로 여러 개를 붙일 필요 없이 한 번에 필드들을 이어 붙여주기 때문에 편리하다. 
<hide/>
@Getter
@Setter
@Value
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public class BaseUtil implements Serializable {
    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
    }
}

 

 

  • PageUtil
    • 주의) 해당 클래스는 프론트에서 url에 변수와 값을 param으로 넣어서 보내주기 때문에 변수명이 일치해야한다. 
    • BaseUtil 클래스를 상속한다. 
    • curPage: 현재 페이지 번호 (프론트에서 해당 정보를 보내준다. )
    • paging:  목록에 대상 객체가 하나 이상 존재하면 true로 설정한다. 
    • pageListSize: 한 페이지당 보여줄 엔티티의 개수
    • totalListSize: DB에 저장된 총 엔티티의 개수
    • boolean paging은 totalListSize가 0인지 아닌지 여부로 판단한다. 
    • totalPage: 총 페이지 개수 (총 엔티티 개수를 10으로 나눠서 올림한 것과 같다.)
<hide/>
@Getter
@Setter
public class PageUtil extends BaseUtil {
    private int currPage = 1;
    private int pageListSize = 10;  // 한 페이지에 나타낼 model 개수
    private int startIndex;
    private long totalListSize; // 모든 model 개수
    private long totalPage; // 모든 페이지 수
    private boolean paging = false;

    public void setTotalListSize(long totalListSize) {
        if (totalListSize > 0) {
            this.totalListSize = totalListSize;
            this.startIndex = (this.currPage - 1) * this.pageListSize;
            this.totalPage = this.totalListSize % this.pageListSize == 0 ? this.totalListSize / this.pageListSize : this.totalListSize / pageListSize + 1;     // 45개인경우는 5개 페이지 필요하고 , 40개의  경우 4개가 필요하다.
        }
    }
}

 

 

  • BeanUtils
    • 스프링에서 제공하는 클래스
    • BeanUtils.copyProperties(source, target): source(원본 객체), target(복사 대상 객체)
      • 필드의 개수가 많을 때 어떤 객체를 다른 객체에 복사할 때 사용된다. 
      • 예를 들어, Dto를 Entity로 바꾸거나 그 반대의 경우에 쓰면 편리하다. 
      • source 안에 getter(), target 안에는 setter()가 존재해야한다. 

 

 

  • SearchUtil 클래스는 PageUtil 클래스를 상속한다.
    • searchLimit 안에는 데이터를 보여주기 위한 시작, 마지막 인덱스가 들어간다.  
<hide/>
@Data
public class SearchUtil extends PageUtil {

    private String searchRegSt;
    private String searchRegEd;
    private String searchAmdSt;
    private String searchAmdEd;

    @Value("[]")
    private int[] searchLimit;

}

 

 

  • Query 클래스
    • 다음과 같이 페이징 정보를 넣어서 메서드에 쿼리 정보를 넣어준다. 
<hide/>
<hide/>

@Component
@RequiredArgsConstructor
public class ModelQuery {

    private final JPAQueryFactory queryFactory;
    private final QModel model = QModel.model;
	...
    public List<Model> getListByPaging(Model modelVO) {
        int offset, limit;
        List<Model> resultList;
        offset = (modelVO.getCurPage() - 1) * modelVO.getPageListSize();
        limit = modelVO.getPageListSize();
        resultList = queryFactory.select((projection()))
                .from(modelVO)
                .where(predicate(modelVO))
                .orderBy(modelVO.modelId.asc())
                .offset(offset)
                .limit(limit)
                .fetch();
        return resultList;
    }
}

 

참고)  https://github.com/goraneee/zerobase_backendschool2_KimRan_part8_mission/blob/master/src/main/java/com/zerobase/fastlms/util/PageUtil.java

 

 


LocalDate <=> String으로 바꾸기

 

  • String => LocalDate 
    • DateTimeFormatter를 이용한다. 
    • modelVO는 프론트에서 넘어온 데이터로 model에 관한 정보를 String 형태로 담고 있다. 
<hide/>
String startDtOfInput = modelVo.getSearchRegSt();
String endDtOfInput = modelVo.getSearchRegEd();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate startDt = LocalDate.parse(startDtOfInput, formatter);
LocalDate endDt = LocalDate.parse(endDtOfInput, formatter);

 

  • LocalDate => String  
    • LocalDate 형태의 변수에 toString()을 붙이면 변환 완료!

 

 

에러 해결


404 Not Found

 

  • 오류: 404 NOT FOUND
  • 원인: GET  요청을 보낼 때 Header에 content-length가 체크되어 있음.  
  • 해결: content-length 체크 해제한다. content-length는 POST 요청을 보낼 때 필요하다. 

JpaObjectRetrievalFailureException , NoSuchElementException - JPA 관련

 

 "org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find kr.co.Model with id 아이디; nested exception is javax.persistence.EntityNotFoundException: 
  "trace": "java.lang.RuntimeException: modelId에 대해 존재하는 ModelParam 가 없습니다.\r\n\tat kr.co.ModelParamService.getModelParam(ModelService.java:34)\r\n\tat kr.co.
  • 오류: 저장해둔 값에 대해
    • findById() JPA 메서드가 안 먹힌다. 
  • 원인
    • model <=> modelParam 이 일대다 매핑이 되어있다. 
    • modelParam 테이블에는 (FK) modelId= 3인 여러 행을 추가했으나  model 테이블에는 (PK) modelId = 3인 행이 존재하지 않아서 문제가 생김
    • 즉, 연관관계 매핑을 해뒀으나 FK에 연결된 PK 값이 주 테이블에 존재하지 않아서 문제가 발생
  • 해결
    • modelParam 에 FK로 넣은 model_id 내용을 model 테이블에도  PK 값을 넣어서 서로 연결되어 있도록 해준다. 
    • findById() 안에 다른 테이블의 PK 값이 들어가는 경우에 다른 테이블에도 해당 데이터가 들어가 있어야한다.  가장 조회 메서드에서 가장 먼저 modelRepository.findById()를 하는데 이 테이블에 modelId가 없으면 당연히 null이기 때문이다. 

 


찾아 보기

  • QueryDSL 세팅 방법