> Hello World !!!

     

@syaku

spring boot jpa 스프링 부트 블로그 만들기 #2 - blog

스프링 부트 블로그 만들기 #2

Github: https://github.com/syakuis/syaku-blog

모든글

변경사항

  • 2018-10-24 - 리팩토링

    • Post 인터페이스 제거 대신 PostEntity Setter 비활성화한다. 필요할때 활성화한다.

    • Entity Date 형식을 TimeStemp 형식으로 변경.

    • 날짜 값 자동 주입을 PrePresist 대신 @CreationTimestamp로 변경.

    • id long 에서 Long 형식으로 변경.

이어서 페이지네비게이션을 구현해본다.

spring-data-jpa 라이브러리에 보면 PagingAndSortingRepository 라는 인터페이스를 확인해보면 아래와 같은 기능을 확인할 수 있다.

  /**
* Returns a {@link Page} of entities meeting the paging restriction provided in the {@code Pageable} object.
*
* @param pageable
* @return a page of entities
*/
Page<T> findAll(Pageable pageable);

위 클래스를 상속받아서 사용하거나 직접 구현해도 된다.

findAll 기능에 매개변수는 하나뿐이라서 정렬 상태는 Pageable 에 설정할 수 있다.

  @Test
 public void 페이지네비게이션_테스트() {
   Page<PostEntity> pagePost = postService.getPostPaging();
   assertEquals(pagePost.getTotalPages(), 100);
   assertEquals(pagePost.getSize(), 10);
   assertEquals(pagePost.getTotalElements(), 1000);
}

서비스와 리포지토리도 같이 구현한다.

  // service
 public Page<PostEntity> getPostPaging() {
   return getPostPaging(PageRequest.of(0, 10, new Sort(Sort.Direction.DESC, "id")));
}

 public Page<PostEntity> getPostPaging(Pageable pageable) {
   Page<PostEntity> page = postRepository.findAll(pageable);
   return new PageImpl<>(getImmutable(page.getContent()), pageable, page.getTotalElements());
}
 
// repository
Page<PostEntity> findAll(Pageable pageable);

테스트를 실행한다. 다음은 검색 기능을 구현해본다.

  @Test
 public void 제목_검색_테스트() {
   Page<PostEntity> pagePost = postService.getPostPaging("search");
   assertEquals(pagePost.getSize(), 10);
   assertEquals(pagePost.getTotalElements(), 500);
   assertEquals(pagePost.getTotalPages(), 50);
}

포스트 데이터중 search 키워드를 검색했다. 임시 테스트 데이터는 개발자가 어떠한 조건에서든 결과를 예측할 수 있어야 테스트를 검증할 수 있다.

테스트 구현에 맞게 서비스와 리포지토리도 수정한다.

  // 서비스
 public Page<PostEntity> getPostPaging() {
   return getPostPaging(null);
}

 public Page<PostEntity> getPostPaging(String subject) {
   return getPostPaging(subject,
     PageRequest.of(0, 10, new Sort(Sort.Direction.DESC, "id")));
}

 public Page<PostEntity> getPostPaging(String subject, Pageable pageable) {
   Page<PostEntity> page = StringUtils.isEmpty(subject) ?
     postRepository.findAll(pageable) :
     postRepository.findAllBySubjectContaining(subject, pageable);
   return new PageImpl<>(getImmutable(page.getContent()), pageable, page.getTotalElements());
}
 
 // 리포지토리
 Page<PostEntity> findAllBySubjectContaining(String subject, Pageable pageable);

테스트를 실행한다.

Like 참고 ㅣ https://docs.spring.io/spring-data/jpa/docs/2.1.1.RELEASE/reference/html/#jpa.query-methods.query-creation

테스트를 마친 서비스의 기능들을 컨트롤러에 반영하고 테스트를 진행해본다.

우선 페이지네비게이션처리 작업부터 테스트를 작성한다. 기존에 컨트롤러 목록은 페이지네비게이션 기능으로 구현되지 않았다. 이부분을 변경하기 전에 PostContollerTest 에서 하나에 기능에 3가지 테스트를 한번에 처리하게 구현되어있다. 이런 부분은 가독성도 떨어지지고 한가지 테스트에 집중할 수 없으니 테스트를 작게 세분화한다.

< / > PostContollerTest.java


 @Before
 public void setup() {
   assertNotNull(postService);
   objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
   objectMapper.registerModule(new JavaTimeModule());
   postService.save(PostEntity.builder().subject("제목").contents("내용").build());
}

 @Test
 public void 쓰기() throws Exception {
   this.mvc.perform(
     post("/post")
      .content(objectMapper.writeValueAsString(
         PostEntity.builder().subject("쓰기_제목").contents("쓰기_내용").build()))
      .contentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_UTF8_VALUE))
  )
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_UTF8_VALUE)))
    .andExpect(jsonPath("$.subject").value("쓰기_제목"))
    .andExpect(jsonPath("$.contents").value("쓰기_내용"));
}

 @Test
 public void 목록() throws Exception {
   this.mvc.perform(
     get("/post")
  )
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_UTF8_VALUE)))
    .andExpect(content().json(objectMapper.writeValueAsString(postService.getPostPaging())));
}

 @Test
 public void 보기() throws Exception {
   PostEntity post = postService.getPost(1);

   this.mvc.perform(get("/post/{id}", 1))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_UTF8_VALUE)))
    .andExpect(content().json(objectMapper.writeValueAsString(post)));
}

 @Test
 public void 삭제() throws Exception {
   assertNotNull(postService.getPost(1));
   this.mvc.perform(delete("/post/{id}", 1))
    .andExpect(status().isOk());

   assertNull(postService.getPost(1));
}

< / > PostController.java

  @GetMapping("")
 public Page<PostEntity> list() {
   return postService.getPostPaging();
}

테스트를 실행한다. 다음은 포스트 검색과 페이지 변경 기능을 구현한다.

< / > PostListControllerTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class PostListControllerTest {
 @Autowired
 private MockMvc mvc;

 @Autowired
 private PostService postService;

 // deleteAll 기능이 서비스에 구현되어 있지 않아 불러와서 사용했다.
 // 테스트가 아닌 곳에서 리포지토리는 서비스에만 주입될 수 있도록 작업해야 한다.
 @Autowired
 private PostRepository postRepository;

 @Before
 public void setup() {
   StringBuilder stringBuilder = new StringBuilder();
   List<PostEntity> postEntities = new ArrayList<>();
   for (int i = 0; i < 1000; i++) {
     postEntities.add(PostEntity.builder()
      .subject(stringBuilder.append("a").append(i).append(i % 2 == 0 ? "search" : "").toString())
      .contents("내용").build());
     stringBuilder.setLength(0);
  }
   
   this.postService.save(postEntities);
}

 @After
 public void exit() {
   postRepository.deleteAll();
}

 @Test
 public void 페이징_테스트() throws Exception {
   this.mvc.perform(get("/post"))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
    .andExpect(jsonPath("$.content", hasSize(10)))
    .andExpect(jsonPath("$.content[0].subject", is("a999")))
    .andExpect(jsonPath("$.totalElements", is(1000)))
    .andExpect(jsonPath("$.totalPages", is(100)));
}

 @Test
 public void 검색_테스트() throws Exception {
   this.mvc.perform(get("/post").param("subject", "search"))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
    .andExpect(jsonPath("$.content", hasSize(10)))
    .andExpect(jsonPath("$.content[0].subject", is("a998search")))
    .andExpect(jsonPath("$.totalElements", is(500)))
    .andExpect(jsonPath("$.totalPages", is(50)));
}
}

포스트의 id 는 데이터베이스 시퀀스를 이용하여 1씩 증가된다. 그리고 테스트를 일괄적으로 모두 실행하게 되면 데이터가 계속 쌓이게 된다. 그래서 @After 을 이용하여 하나의 테스트가 끝나면 데이터를 삭제되도록 추가하였다.

리포지토리에 포스트 모두 삭제 기능을 추가한다.

< / > PostRepository.java

void deleteAll();

이제 컨트롤러에 없는 검색 기능을 추가한다.

< / > PostController.java

  @GetMapping(value = "", params = "subject")
 public Page<PostEntity> search(
   @RequestParam(name = "subject", defaultValue = "", required = false) String subject) {
   return postService.getPostPaging(subject);
}

테스트를 실행한다.

만약 검색어가 없다면 어떤 결과가 나올까? 테스트 작성해본다.

  @Test
 public void 검색어_없는_테스트() throws Exception {
   this.mvc.perform(get("/post").param("subject", ""))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
    .andExpect(jsonPath("$.content", hasSize(0)))
    .andExpect(jsonPath("$.totalElements", is(0)))
    .andExpect(jsonPath("$.totalPages", is(0)));
}

테스트를 실행한다. 결과는 실패이다. 검색어가 없으면 데이터가 없어야 정상인데 데이터가 존재한다.

java.lang.AssertionError: JSON path "$.content"
Expected: a collection with size <0>
    but: collection size was <10>

문제는 서비스에서 검색어가 없으면 전체 목록을 반환하도록 하였다.

  public Page<PostEntity> getPostPaging(String subject, Pageable pageable) {
   Page<PostEntity> page = StringUtils.isEmpty(subject) ?
     postRepository.findAll(pageable) :
     postRepository.findAllBySubjectContaining(subject, pageable);
   return new PageImpl<>(getImmutable(page.getContent()), pageable, page.getTotalElements());
}

이 기능은 검색을 위한 것이 아니라 포스트 페이징 기능을 위해 존재한다. 그래서 검색은 다른 기능으로 분리한다.

< / > PostController.java

  @GetMapping(value = "", params = "subject")
 public Page<PostEntity> search(
   @RequestParam(name = "subject", defaultValue = "", required = false) String subject) {
   return postService.getSearchPostPaging(subject);
}

포스트 서비스의 목록 기능들을 리팩토링하였다.

< / > PostService.java

  private Sort getSort() {
     return new Sort(Sort.Direction.DESC, "id");
}

 private Pageable getPageable() {
   return PageRequest.of(0, 10, getSort());
}

 public List<PostEntity> getPostList() {
   return this.getPostList(getSort());
}

 public List<PostEntity> getPostList(Sort sort) {
   return getImmutable(postRepository.findAll(sort));
}

 public Page<PostEntity> getPostPaging() {
   return getPostPaging(getPageable());
}

 public Page<PostEntity> getPostPaging(Pageable pageable) {
   Page<PostEntity> page = postRepository.findAll(pageable);
   return new PageImpl<>(getImmutable(page.getContent()), pageable, page.getTotalElements());
}

 public Page<PostEntity> getSearchPostPaging(String subject) {
   return getSearchPostPaging(subject, getPageable());
}

 public Page<PostEntity> getSearchPostPaging(String subject, Pageable pageable) {
   if (StringUtils.isEmpty(subject)) {
     return new PageImpl<>(Collections.emptyList(), pageable, 0);
  }

   Page<PostEntity> page = postRepository.findAllBySubjectContaining(subject, pageable);
   return new PageImpl<>(getImmutable(page.getContent()), pageable, page.getTotalElements());
}

반복적으로 사용하는 sort 와 pageable 는 기능으로 분리하였다. 그리고 검색용 getSearchPostPaging 추가하였다.

그리고 기존에 getPostPaging 에 검색을 위한 제목 매개변수를 제거한다.

모든 테스트 실행한다. 모든 테스트를 실행하려면 아래와 같이 테스트 소스 모두를 포함한 상위 package 경로에 마우스를 가져가 오른쪽을 누르고 테스트를 실행하면 된다.

마지막으로 페이지 번호를 변경했을때에 대한 테스트를 해보자.

< / > PostListControllerTest.java

  @Test
 public void 페이지_변경_테스트() throws Exception {
   this.mvc.perform(get("/post").param("page", "50"))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
    .andExpect(jsonPath("$.content", hasSize(10)))
    .andExpect(jsonPath("$.content[0].subject", is("a499")))
    .andExpect(jsonPath("$.number", is(50)))
    .andExpect(jsonPath("$.totalElements", is(1000)))
    .andExpect(jsonPath("$.totalPages", is(100)));
}

 @Test
 public void 검색_페이지_변경_테스트() throws Exception {
   this.mvc.perform(get("/post")
    .param("subject", "search")
    .param("page", "20"))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
    .andExpect(jsonPath("$.content", hasSize(10)))
    .andExpect(jsonPath("$.content[0].subject", is("a598search")))
    .andExpect(jsonPath("$.number", is(20)))
    .andExpect(jsonPath("$.totalElements", is(500)))
    .andExpect(jsonPath("$.totalPages", is(50)));
}

페이지 변경기능이 추가되었으니 컨트롤러에도 수정한다.

페이지 번호를 넘겨줘야 하고 검색 값도 넘겨줘야 한다. 포스트 검색 클래스를 만들어 한번에 넘기도록 수정한다.

< / > PostController.java

  @GetMapping("")
 public Page<PostEntity> list(
   @RequestParam(name = "page", defaultValue = "0", required = false) int page) {
   return postService.getPostPaging(page);
}

 @GetMapping(value = "", params = "subject")
 public Page<PostEntity> search(
   @RequestParam(name = "page", defaultValue = "0", required = false) int page,
   @RequestParam(name = "subject", defaultValue = "", required = false) String subject) {
   return postService.getSearchPostPaging(subject, page);
}

페이지 번호를 추가한 기능 서비스도 추가한다.

< / > PostService.java

  private Pageable getPageable() {
   return getPageable(0);
}
 private Pageable getPageable(int page) {
   return PageRequest.of(page, 10, getSort());
}

 public Page<PostEntity> getPostPaging(int page) {
   return getPostPaging(getPageable(page));
}

public Page<PostEntity> getSearchPostPaging(String subject, int page) {
   return getSearchPostPaging(subject, getPageable(page));
}

모든 테스트를 실행한다.

모든 테스트를 마쳤다.