이번 시간에는 게시글 조회 API를 만들어보겠습니다.
먼저 repository.post.PostRepository에 다음과 같은 쿼리를 작성해줍니다.
package kukekyakya.kukemarket.repository.post;
...
public interface PostRepository extends JpaRepository<Post, Long> {
@Query("select p from Post p join fetch p.member where p.id = :id")
Optional<Post> findByIdWithMember(Long id);
}
각 게시글을 조회할 때는 작성자의 정보도 보내줄 것이기 때문에, fetch join을 이용하여 Member도 함께 조회해줍니다.
작성된 쿼리를 테스트해봅시다.
PostRepositoryTest에 새로운 테스트를 작성해줍니다.
// PostRepositoryTest.java
@Test
void findByIdWithMemberTest() {
// given
Post post = postRepository.save(createPost(member, category));
// when
Post foundPost = postRepository.findByIdWithMember(post.getId()).orElseThrow(PostNotFoundException::new);
// then
Member foundMember = foundPost.getMember();
assertThat(foundMember.getEmail()).isEqualTo(member.getEmail());
}
로그를 확인해보면, 1개의 쿼리만 나가는 것을 확인할 수 있습니다.
이제 service.post.PostService에 read 메소드를 작성해주겠습니다.
// PostService.java
public PostDto read(Long id) {
return PostDto.toDto(postRepository.findById(id).orElseThrow(PostNotFoundException::new));
}
단순히 파라미터로 전달 받은 id를 이용하여 조회된 Post를, PostDto로 변환하여 반환해줍니다.
dto.post.PostDto는 다음과 같습니다.
package kukekyakya.kukemarket.dto.post;
import ...
@Data
@AllArgsConstructor
public class PostDto {
private Long id;
private String title;
private String content;
private Long price;
private MemberDto member;
private List<ImageDto> images;
public static PostDto toDto(Post post) {
return new PostDto(
post.getId(),
post.getTitle(),
post.getContent(),
post.getPrice(),
MemberDto.toDto(post.getMember()),
post.getImages().stream().map(i -> ImageDto.toDto(i)).collect(toList())
);
}
}
작성자의 정보도 함께 가지고 있습니다.
각 게시글이 가지고 있는 이미지에 대한 정보도 반환해줍니다.
이번에는 dto.post.ImageDto를 살펴보겠습니다.
package kukekyakya.kukemarket.dto.post;
import ...
@Data
@AllArgsConstructor
public class ImageDto {
private Long id;
private String originName;
private String uniqueName;
public static ImageDto toDto(Image image) {
return new ImageDto(image.getId(), image.getOriginName(), image.getUniqueName());
}
}
원래의 파일 명과 서버에서 생성한 고유한 파일 명으로 DTO를 생성해서 반환해줍니다.
이제 PostService에 새롭게 작성된 read 메소드에 대해 테스트해봅시다.
PostServiceTest에 다음과 같은 두 개의 테스트를 추가해줍니다.
// PostServiceTest.java
@Test
void readTest() {
// given
Post post = createPostWithImages(List.of(createImage(), createImage()));
given(postRepository.findById(anyLong())).willReturn(Optional.of(post));
// when
PostDto postDto = postService.read(1L);
// then
assertThat(postDto.getTitle()).isEqualTo(post.getTitle());
assertThat(postDto.getImages().size()).isEqualTo(post.getImages().size());
}
@Test
void readExceptionByPostNotFoundTest() {
// given
given(postRepository.findById(anyLong())).willReturn(Optional.ofNullable(null));
// when, then
assertThatThrownBy(() -> postService.read(1L)).isInstanceOf(PostNotFoundException.class);
}
정상적인 조회와 없는 게시글 조회에 대해서 테스트해주었습니다.
자세한 설명은 생략하겠습니다.
다음으로 controller.post.PostController에 read를 수행하기 위한 웹 API를 추가해줍니다.
// PostController.java
@ApiOperation(value = "게시글 조회", notes = "게시글을 조회한다.")
@GetMapping("/api/posts/{id}")
@ResponseStatus(HttpStatus.OK)
public Response read(@ApiParam(value = "게시글 id", required = true) @PathVariable Long id) {
return Response.success(postService.read(id));
}
게시글 id로 조회된 결과를 응답해줍니다.
게시글 조회는 모든 사용자가 요청할 수 있어야하고, /api/** 요청에 대해서는 permitAll로 설정해두었으므로, 이번에는 별다른 security 설정을 하지 않아도 되겠습니다.
PostController에 새롭게 작성된 read에 대해서 테스트해주겠습니다.
PostControllerTest, PostControllerAdviceTest, PostControllerIntegrationTest에 각각 다음과 같은 테스트를 추가해줍니다.
// PostControllerTest.java
@Test
void readTest() throws Exception {
// given
Long id = 1L;
// when, then
mockMvc.perform(
get("/api/posts/{id}", id))
.andExpect(status().isOk());
verify(postService).read(id);
}
// PostControllerAdviceTest.java
@Test
void readExceptionByPostNotFoundTest() throws Exception {
// given
given(postService.read(anyLong())).willThrow(PostNotFoundException.class);
// when, then
mockMvc.perform(
get("/api/posts/{id}", 1L))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(-1012));
}
// PostControllerIntegrationTest.java
@Test
void readTest() throws Exception {
// given
Post post = postRepository.save(createPost(member, category));
// when, then
mockMvc.perform(
get("/api/posts/{id}", post.getId()))
.andExpect(status().isOk());
}
자세한 설명은 생략하겠습니다.
모든 테스트가 정상적으로 수행되었습니다.
포스트맨을 이용하여 게시글을 직접 작성하고, 작성된 게시글을 조회해보겠습니다.
위와 같이 두 개의 이미지를 첨부하여 게시글을 생성하였고, 생성된 게시글의 id를 응답받았습니다.
2번 게시글을 조회해보겠습니다.
GET /api/posts/2
{
"success": true,
"code": 0,
"result": {
"data": {
"id": 2,
"title": "글제목 입니다 ~~",
"content": "글 내용입니다 ~~",
"price": 1500,
"member": {
"id": 1,
"email": "admin@admin.com",
"username": "admin",
"nickname": "admin"
},
"images": [
{
"id": 1,
"originName": "1.PNG",
"uniqueName": "5d0b1b63-f4e2-481c-b81c-28d079cbffff.PNG"
},
{
"id": 2,
"originName": "2.PNG",
"uniqueName": "91e2c8fe-cfea-42dd-9200-e71626f7086f.PNG"
}
]
}
}
}
2번 게시글에 대한 정보를 응답 받았습니다. 작성자의 정보도 확인할 수 있습니다.
이미지의 uniqueName으로 이미지에 접근해보겠습니다.
응답받은 이미지 경로에 대해 정상적인 접근도 확인할 수 있었습니다.
* 누락됐었던 내용 추가입니다.
dto.post.PostDto가 게시글 작성일자와 수정일자도 포함될 수 있도록 코드를 수정해줍니다.
@Data
@AllArgsConstructor
public class PostDto {
private Long id;
private String title;
private String content;
private Long price;
private MemberDto member;
private List<ImageDto> images;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime createdAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime modifiedAt;
...
}
응답 바디로 전송하기 위해 JSON으로 변환될 때, @JsonFormat으로 지정해둔 형태로 LocalDateTime이 변환되어 응답됩니다.
이번 시간에는 게시글 조회 API를 작성해보았습니다.
다음 시간에는 게시글 삭제 API를 작성해보겠습니다.
* 질문 및 피드백은 환영입니다.
* 전체 소스코드에서는 여기에서 확인해볼 수 있습니다.
'Spring > 게시판 만들기' 카테고리의 다른 글
스프링부트 게시판 API 서버 만들기 (24) - 게시글 - 수정 (0) | 2021.12.14 |
---|---|
스프링부트 게시판 API 서버 만들기 (23) - 게시글 - 삭제 (0) | 2021.12.13 |
스프링부트 게시판 API 서버 만들기 (21) - 게시글 - 생성 (4) | 2021.12.12 |
스프링부트 게시판 API 서버 만들기 (20) - 게시글 - 엔티티 설계 (0) | 2021.12.10 |
스프링부트 게시판 API 서버 만들기 (19) - Entity Graph로 LAZY 전략에서 N + 1 문제 해결 (0) | 2021.12.08 |