개인 프로젝트/예약 시스템 개발하기

예약 시스템 개발하기 04. JPA 쿼리 메소드와 JPQL , CharacterEncodingFilter

going.yoon 2022. 3. 6. 23:48

예약 시스템에서 사용자가 화면에서 수업을 예약/ 취소 할 경우 데이터를 핸들링하는 로직을 작성해보았다. 테스트 코드까지 완성시키는데 꽤 오래 걸렸다.

 

 

사용자가 예약 가능한 수업을 리스트를 조회하는 메소드를 테스트하는 로직이다. 멤버십 아이디에 대해서는 PathVariable로 받고 있기 때문에 기본 api 경로인 /reservation뒤에 1로 셋팅하고, 다른 유저아이디나 센터아이디, 기준일자에 대해서는 RequestParameter로 받고 있기 때문에 mockMvc의 Param으로 셋팅한다.

@Test
public void getCourseList() throws Exception{
    MultiValueMap<String, String> param = new LinkedMultiValueMap<>();

    param.add("userId", "1");
    param.add("cntrId","1231");
    param.add("baseDt","20220224");

    mockMvc.perform(get("/reservation/1")
                    .params(param))
                    .andExpect(status().isOk())
                    .andDo(print());
}

 

문제는 바로  이 날짜를 설정하는데 있었다. String으로 받은 파라미터를 LocalDate형식으로 전환해주어야 하기 때문이다.

param.add("baseDt","20220224");

 

이 문제는 DateTimeFormatter를 사용해 String 을 파싱해주면서 해결하였다.

// CourseReservationService.java 일부

@Transactional
    public List<CourseDTO> getCourseList (int mmbrshpId, int userId, int cntrId, String baseDt){
        LocalDate baseLocalTime = LocalDate.parse(baseDt, DateTimeFormatter.ofPattern("yyyyMMdd")); // # 1. DateTimeFormatter사용
        //.withLocale(Locale.KOREA)); // # 2. with locale info or not
        return courseRepository.findByCrsStrtDtAfterAndCrsEndDtBeforeAndCntrId(baseLocalTime,baseLocalTime.plusDays(1),cntrId).stream()
                .map(course -> new CourseDTO(course)).collect(Collectors.toList()); // 기준일자의 수업 리스트 조회
    }

 

이 경우에 수업을 찾아오는 조건은 두가지가 있다.

1. 기준일자가 수업시작일자보다 크거나 같고 수업종료일자보다 작거나 같아야 함.

2. 해당 수업이 열리는 CenterId와 유저가 가지고 있는 멤버십 객체 안의 CenterId값이 일치해야 함.

 

 

처음에는 Repository에서 아래와 같이 ParameterMapping방식으로 쿼리를 작성하려고 했으나 이정도의 메소드도 구현해놓지 않을 JPA가 아니었다. 1번의 조건을 StrtDtAfter , EndDtBefore로 , 2번 조건을 CntrId로 자바 메소드로 해결 가능. 처음에 해결하려고 했던 jpql방식과 JPA 메소드 방식에 대한 정리는 아래 링크를 걸어두었다.

@Repository
public interface CourseRepository extends JpaRepository<Course,Long> , JpaSpecificationExecutor<Course>{

//    @Query( value = "SELECT * from COURSE p where " +
//            "DATE_FORMAT ( :baseDt , '%Y%m%d' ) " +
//            "between date_format(p.crs_strt_dt,'%Y%m%d')  and date_format(p.crs_end_dt,'%Y%m%d') " +
//            "and p.cntr_id = :cntrId"
//    )
//    public List<Course> getCourseListOfTheday(@Param("baseDt") String baseDt , @Param("cntrId") int cntrId);

    public List<Course> findByCrsStrtDtAfterAndCrsEndDtBeforeAndCntrId(LocalDate startDate, LocalDate endDate, Integer cntrId);

}

 

https://jang8584.tistory.com/282

 

[jpa] jpql 문법 정리

JPA에서 현재까지 사용했던 검색 은 아래와 같다. 식별자로 조회 EntityManager.find() 객체 그래프 탐색 e.g. a.getB().getC() 하지만 현실적으로 이 기능만으로 어플리케이션을 개발하기에는 무리이다. 그

jang8584.tistory.com

https://happygrammer.tistory.com/158

 

JPA - 스프링 데이터 JPA에서 쿼리 메소드 안에 지원되는 키워드

쿼리 메소드는 스프링 데이터 JPA의 핵심적인 기능중 하나로 메소드 이름으로 쿼리를 생성할 수 있다는 장점이 있다. 메소드 이름으로 쿼리를 생성을 위해 인터페이스에서 사용할 사용자 쿼리

happygrammer.tistory.com

 

 

 

테스트 실행결과 콘솔에 찍힌 쿼리 및 MVC Request, Response응답값은 다음과 같다.

더보기

//console log 쿼리

 

select course0_.crs_id as crs_id1_6_, course0_.FST_CRT_DT as fst_crt_2_6_, course0_.LST_MDF_DT as lst_mdf_3_6_, course0_.cntr_id as cntr_id4_6_, course0_.crs_dwk as crs_dwk5_6_, course0_.crs_end_dt as crs_end_6_6_, course0_.crs_hr as crs_hr7_6_, course0_.crs_nm as crs_nm8_6_, course0_.crs_strt_dt as crs_strt9_6_, course0_.crs_strt_hh as crs_str10_6_, course0_.crs_strt_mi as crs_str11_6_, course0_.mbr_num as mbr_num12_6_, course0_.reg_num as reg_num13_6_, course0_.thcr_id as thcr_id14_6_ from course course0_ where course0_.crs_strt_dt>'2022-02-24T00:00:00.000+0900' and course0_.crs_end_dt<'2022-02-25T00:00:00.000+0900' and course0_.cntr_id=1231;

 

//MockMvcTest Print

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /reservation/1
       Parameters = {userId=[1], cntrId=[1231], baseDt=[20220224]}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = com.api.euljiro.controller.CourseReservationController
           Method = com.api.euljiro.controller.CourseReservationController#getCourseList(int, int, int, String)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = application/json
             Body = []
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

 

test passed

 

그런데 이 방법은 Body 값에 아무것도 안찍혀서 나온다. 기준일자가 수업시작일자보다 같거나 크고, 수업 종료일자보다 작거나 같아야 하기 때문이다. 아래와 같이 JPA 쿼리 메소드를 After, Before 에서 GreaterThanEqual, LessThanEqual로 바꾸어주었다.

@Repository
public interface CourseRepository extends JpaRepository<Course,Long> , JpaSpecificationExecutor<Course>{

//    @Query( value = "SELECT * from COURSE p where " +
//            "DATE_FORMAT ( :baseDt , '%Y%m%d' ) " +
//            "between date_format(p.crs_strt_dt,'%Y%m%d')  and date_format(p.crs_end_dt,'%Y%m%d') " +
//            "and p.cntr_id = :cntrId"
//    )
//    public List<Course> getCourseListOfTheday(@Param("baseDt") String baseDt , @Param("cntrId") int cntrId);

    //public List<Course> findByCrsStrtDtAfterAndCrsEndDtBeforeAndCntrId(LocalDate startDate, LocalDate endDate, Integer cntrId);
    public List<Course> findByCrsStrtDtGreaterThanEqualAndCrsEndDtLessThanEqualAndCntrId(LocalDate StrtDt, LocalDate endDate, Integer cntrId);
}

 

콘솔에 찍힌 쿼리가 변경되었다.
Body에 값도 잘 찍힌다. 그런데 한글인 수업명이 깨져서 나온다.

 

이는 테스트용 MockMvc를 Build할 때 사용하는 @AutoConfigureMockMvc에 어떠한 인코딩(필터값)이 설정되어있지 않기 때문이다. @AutoConfigureMockMvc 는 MockMvc를 빌드할 때 SpringBootMockMvcBuilderCustomizer를 사용하여 Build 설정값을 커스터마이징하는데, 빈으로 등록된 CharacterEncodingFilter 가 있으면 해당 설정값을 읽어 MockMvc 객체를 생성한다.

 

따라서 @AutoConfigureMockMvc로 구현하고 CharacterEncodingFilter를 Bean으로 등록한 @EnableMockMvc라는 애노테이션을 하나 생성 한 후 AutoConfigureMockMvc대신 사용하기로 한다.

 

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AutoConfigureMockMvc
@Import(EnableMockMvc.Config.class)
public @interface EnableMockMvc {
    class Config {
        @Bean
        public CharacterEncodingFilter characterEncodingFilter() {
            return new CharacterEncodingFilter("UTF-8", true);
        }
    }
}

 

한글 깨짐 해결!