전체 코드는 여기에서 볼 수 있습니다.

오늘은 게시물에 댓글 기능을 적용해보도록 하겠습니다.

- Controller, Service, Entity, Repository, DTO

- CommentController

@RestController
@RequiredArgsConstructor
public class CommentController {

    private final CommentService commentService;

    @PostMapping("/post/{id}/comment")
    @ResponseStatus(value = HttpStatus.OK)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "댓글 쓰기", notes = "id에 해당하는 게시글에 댓글을 작성합니다.")
    public SuccessResponse<String> createComment(@PathVariable(name = "id") Long postId,
                                                 @Valid @RequestBody CommentDTO commentDTO,
                                                 Principal principal) {

        CommentDTO comment = commentService.createComment(postId, commentDTO, principal.getName());

        return SuccessResponse.success(null);

    }
}

- CommentService

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommentService {

    private final CommentRepository commentRepository;
    private final PostRepository postRepository;
    private final UserRepository userRepository;

    @Transactional
    public CommentDTO createComment(Long postId, CommentDTO commentDTO, String email) {
        Optional<Post> byId = postRepository.findById(postId);

        Post post = byId.orElseThrow(() -> new PostNotFound("게시물이 삭제되었거나 존재하지 않습니다."));

        Optional<User> byEmail = userRepository.findByEmail(email);

        User user = byEmail.orElseThrow(() -> new UsernameNotFoundException("유저를 찾을 수 없습니다."));

        Comment comment = Comment.builder()
                .contents(commentDTO.getContents())
                .build();

        comment.mappingPostAndUser(post, user);

        Comment saveComment = commentRepository.save(comment);

        return CommentDTO.convertToCommentDto(saveComment);
    }
}

- CommentEntity

@Entity
@Getter
@Builder
@AllArgsConstructor(access = PROTECTED)
@NoArgsConstructor(access = PROTECTED)
public class Comment {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String contents;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "post_id", foreignKey = @ForeignKey(name = "FK_user_comment"))
    private Post post;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "user_id", foreignKey = @ForeignKey(name = "FK_post_comment"))
    private User user;

    public void mappingPostAndUser(Post post, User user) {
        this.post = post;
        this.user = user;

        post.mappingComment(this);
        user.mappingComment(this);
    }
}

- CommentRepository

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
}

- CommentDTO

@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@Data
public class CommentDTO {
    private Long id;

    @NotBlank
    @Length(max = 100)
    private String contents;
    private UserDTO user;

    public static CommentDTO convertToCommentDto(Comment comment) {
        return CommentDTO.builder()
                .id(comment.getId())
                .contents(comment.getContents())
                .user(UserDTO.convertToUserDTO(comment.getUser()))
                .build();
    }

    public static List<CommentDTO> convertToCommentDtoList(List<Comment> commentList) {
        Stream<Comment> stream = commentList.stream();

        return stream.map(CommentDTO::convertToCommentDto).collect(Collectors.toList());

    }
}

 

여기 까지가 Comment 관련 코드이고 이제 Comment와 Post, User와 관계를 지정해주어야 합니다.

Comment와 Post는 N : 1 관계, Comment와 User는 N : 1 관계 입니다. 그러므로 PostEntity와 UserEntity에 연관관계및 연관관계 메서드를 설정해줍니다.

- PostEntity

    @OneToMany(fetch = LAZY, mappedBy = "post")
    private List<Comment> commentList = new ArrayList<>();
    
        public void mappingComment(Comment comment) {
        this.commentList.add(comment);
    }

- UserEntity

@OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
    private List<Comment> commentList = new ArrayList<>();
    
    public void mappingComment(Comment comment) {
        commentList.add(comment);
    }

 

그리고 이제 게시물에 댓글을 달고 게시물을 조회하고 나면 쿼리가 무언가 이상합니다. 

1번 게시물에 1번 유저, 2번 유저, 3번 유저가 댓글을 달경우 1번 게시물을 조회할때 1, 2, 3번 유저를 조회하는 쿼리가 나가기 때문입니다.

이를 해결하기 위해 저번에 작업한 PostRepository에 최적화 작업을 하겠습니다.

- PostRepository

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    @EntityGraph(attributePaths = {"category", "user", "commentList.user"})
    @Override
    Optional<Post> findById(Long postId);

    @EntityGraph(attributePaths = {"user", "category"})
    @Override
    List<Post> findAll(Sort sort);
}

@EntityGraph에 "commentList.user"를 추가해줌으로써 CommentList와 여기에 댓글을 작성한 유저의 정보까지 한번에 가져오므로 쿼리 1번에 모두 조회가 가능합니다.

전체 코드는 여기에서 확인할 수 있습니다.

 

안녕하세요 이번에는 API를 테스트 할 수 있는 Swagger 문서를 추가 해보도록 하겠습니다.

build.gradle 파일에 추가해줍니다.

// swagger
    compile('io.springfox:springfox-swagger2:2.7.0')
    compile('io.springfox:springfox-swagger-ui:2.7.0')

그 다음 SwaggerConfig 파일을 만들어 줍니다.

@Configuration
public class SwaggerConfig {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Spring Boot REST Sample with Swagger")
                .description("API 문서")
                .termsOfServiceUrl("")
                .contact(new Contact("user", "url", "user@example.com"))
                .version("1.0")
                .build();

    }
}

그 다음으로 Controller에 코드를 추가 해줍니다. 저는 PostController에 추가 해주도록 하겠습니다.

@RestController
@RequiredArgsConstructor
@Api(tags = {"게시물 관련 API"})
public class PostController {
    private final PostService postService;

    @GetMapping("post/{id}")
    @ResponseStatus(value = HttpStatus.OK)
    @ApiOperation(value = "게시물 조회", notes = "게시물을 조회합니다.")
    public SuccessResponse<PostDTO> getPost(@PathVariable(name = "id") Long id) {
        PostDTO post = postService.getPost(id);

        return SuccessResponse.success(post);
    }

    @PostMapping("/post")
    @ResponseStatus(value = HttpStatus.CREATED)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "게시물 생성", notes = "게시물을 생성합니다.")
    public SuccessResponse<PostDTO> createPost(@Valid @RequestBody PostDTO postDTO) {
        PostDTO post = postService.createPost(postDTO);

        return SuccessResponse.success(post);
    }

    @PutMapping("post/{id}")
    @ResponseStatus(value = HttpStatus.OK)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "게시물 수정", notes = "게시물을 수정합니다.")
    public SuccessResponse<PostDTO> updatePost(@Valid @RequestBody PostDTO postDTO, @PathVariable(name = "id") Long postId) {
        PostDTO postDTOResponse = postService.updatePost(postId, postDTO);

        return SuccessResponse.success(postDTOResponse);
    }

    @DeleteMapping("post/{id}")
    @ResponseStatus(value = HttpStatus.OK)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "게시물 삭제", notes = "게시물을 삭제합니다.")
    public SuccessResponse<String> deletePost(@PathVariable(name = "id") Long postId) {
        postService.deletePost(postId);
        return SuccessResponse.success(null);
    }

    @GetMapping("/post-category")
    @ResponseStatus(value = HttpStatus.OK)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "게시물 카테고리 조회", notes = "게시물 카테고리를 조회합니다.")
    public SuccessResponse<List<PostCategoryDTO>> postCategory() {
        List<PostCategoryDTO> postCategoryDTOS = postService.postCategoryList();

        return SuccessResponse.success(postCategoryDTOS);
    }
}

@Api : 해당 컨트롤러가 어떤 API 그룹인지 적어주세요.

@ApiOperation : value -> api의 제목? 을 적어주세요.

@ApiOperation : notes -> api에 대한 설명을 적어주세요.

그 후 Main 실행 파일에 가셔서 @EnableSwagger2 를 붙여주세요.

@SpringBootApplication
@EnableSwagger2
public class BoardRestApiApplication {

    public static void main(String[] args) {
        SpringApplication.run(BoardRestApiApplication.class, args);
    }

}

 

저 같은 경우에는 Spring Security를 사용하고 있기 때문에 swagger 관련 주소를 제외시켜주어야 합니다. SecurityConfig에 추가시켜줍니다.

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers("/webjars/**")
                .antMatchers("/v2/api-docs/**")
                .antMatchers("/configuration/**")
                .antMatchers("/swagger*/**");
    }

 

이제 실행하신다음 http://localhost:8080/swagger-ui.html 에 접속하시면 아래와 같은 화면이 나오며 api를 테스트 할 수 있습니다.

+ Recent posts