우리는 게시판 까지 구현 완료 하였지만 이번에는 댓글 다는 기능을 만들어 보자.
다들 알겠지만 우리가 당연하다고 생각하는 것들은 전부 만들어 진것들이다.
먼저 댓글에 대해 생각을 해보자.
1) 댓글은 당연히 게시글에 달려야 겠지?
2) 댓글은 한개의 게시글에 여러개가 달릴수 있겠지?
3) 댓글도 모이면 페이지네이션 처리를 해야겠다.
4) 댓글이 게시글이랑 같이 딸려오면 과부화가 생기겠다.
5) 게시글에 들어가기전에 댓글이 몇개 달렸는지 알고 싶다.
6) 게시글이 삭제되면 댓글도 삭제되야겠다.
처음에 댓글 구현을 할때 내가 실수한게 있다. 바로 게시글을 하나 불러올때 댓글도 같이 불러온다는 것이다. 즉, 같은 api 주소를 사용해서 구현을 했다 Postman으로 결과를 보자.
아래는 1번 게시글을 요청해 본거다.
아래는 전체 게시글을 요청해 본거다.
혹시 무슨 느낌인지 알겠는가?
바로 게시글을 30개를 나열하던 50개를 나열하던 댓글들 까지 함께 불러와서 서버의 자원을 낭비한다는 거다. 그래서 해결법이 뭐냐? api를 나누면 그만이다. 그리고 프론트엔드에서 boardId 1로 묶여있는 reply api 요청을 한번더 보내면 해결 된다.
다시 한번 핵심을 정리하면.
1) 게시글에 들어가면 프론트에서 댓글 요청을 해서 댓글이 뜨게 할꺼다.
2) 댓글들은 페이지네이션 처리가 되어야 한다.
3) 게시글과 댓글이 연관 관계를 가져야 한다.
코드로 살펴보는데 기본 CRUD는 게시판에서 충분히 설명했으므로 설명을 생략하며 작성 하겠다.
먼저 연관 관계 매핑을 해주겠다. 연관관계는 조립이랑 비슷하다. 엔티티들에게 서로 관계를 맺어 주는 것이다.
Board 엔티티를 먼저 보자.
@Entity
@Getter @Setter
@NoArgsConstructor
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long boardId;
private String title;
private String content;
@OneToMany(mappedBy = "board", cascade = {CascadeType.PERSIST,CascadeType.REMOVE})
private List<Reply> reply = new ArrayList<>();
}
여기서 눈에 들어오는건 당연히 이것이다.
@OneToMany(mappedBy = "board", cascade = {CascadeType.PERSIST,CascadeType.REMOVE})
private List<Reply> reply = new ArrayList<>();
private List<Reply> reply = new ArrayList<>(); 이건 연관 관계를 가진 댓글 여러개를 한번에 리스트 형태로 가져올꺼라 사용했다. 혹시 모르면 리스트의 개념은 자바를 살펴보자(내가 자세히 블로그에 올린것도 있음)
이건 JPA에서 제공하는 기능들인데 먼저
@OneToMany는 일대다 라고도 하는데 게시판 한개랑 댓글이 여러개 달리지 않겠는가? 그래서 일대 다수 이다.
mappedBy = "board"는 연관 관계에서 board를 쓰는 녀석을 찾는거다. 그런데 단순히 찾는게 아니라 내가 주인이라는 뜻이다. 뒤의 내용을 보면 이해가 갈꺼다.
cascade = {CascadeType.PERSIST,CascadeType.REMOVE} 도 생소할 꺼다. 이 뜻은 Cascade는 같이 동작을 하는건데
CascadeType.PERSIST = 엔티티가 존재하면 연관관계 물린 녀석도 존재하고
CascadeType.REMOVE = 엔티티가 제거되면 연관관계 물린 녀석도 제거된다.
이해가 가는가? 같이 살고 같이 죽는거다 그런데 생사는 mappedBy를 가지고 있는 주인이 쥐고 있는거다. 추가 예를들어 게시판이 생성되고 댓글이 물리게 되면 쭉 유지가 되고 게시판이 삭제되면 댓글도 같이 삭제되는거다.
중요한 개념이라 직접 보여주겠다.
게시글에 댓글 두개를 달아주겠다.
그리고 게시판을 삭제해 보겠다.
그러면 DB는 어떻게 될까?
댓글들 또한 전부 사라졌다. 이렇게 돌아가는것이다
이젠 Reply 엔티티를 보자.
@Entity
@Getter @Setter
@NoArgsConstructor
public class Reply {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long replyId;
private String reContent;
@ManyToOne
@JoinColumn(name = "board_id")
private Board board;
}
여기는 Board와는 다르게 @ManyToOne으로 되어 있다. 이유는 아까와 같다 다대일.
@JoinColumn(name = "board_id")는 Reply엔티티에 board_id라는 항목을 만들어서 board 값을 저장하는 용도이다. 위의 스샷에서 확인할수 있다.
나머지 CRUD는 대부분 board와 같으나 Get을 한번 살펴보자.
먼저 ReplyService다.
public Page<ReplyResponseDto> findAllReply(Pageable pageable,Long boardId) {
Board board = boardService.findBoardId(boardId);
Page<Reply> replies = replyRepository.findByBoard(board, pageable);
return replies.map(ReplyResponseDto::FindFromReply);
}
비교를 위해 BoardService도 가져왔다.
public Page<BoardResponseDto> findAllBoards(Pageable pageable) {
Page<Board> boards = boardRepository.findAll(pageable);
return boards.map(BoardResponseDto::FindFromBoard);
}
같은 페이지네이션 처리인데 다른점이 있지 않은가?
그렇다. Reply는 boardId를 기반으로 페이지네이션 처리를 하므로 추가 코딩이 필요하다.
1) 당연히 boardId를 끌고 와야한다.
2) Board 엔티티와 연관된 Reply를 페이지네이션 처리해서 가져와야 하는데 코드를 보면 findByBoard라는게 ReplyRepository에 생성되어 있다.
Repository를 보자.
@Repository
public interface ReplyRepository extends JpaRepository<Reply, Long> {
Page<Reply> findByBoard(Board board, Pageable pageable);
}
기존과 다르게 한줄 추가 되었다.
Page<Reply> findByBoard(Board board, Pageable pageable);
findByBoard는 boardId를 인자로 받아서 Reply를 페이지네이션 하는건데.
Page가 뭐라고 했는가? 페이지네이션 결과를 담는거라 했다. 뭐가 담기는가? Reply.
Pageable은? 페이지네이션 처리하는 도구.
여기서 이런 의문이 들수가 있다. 아니 BoardService에서는
public Page<BoardResponseDto> findAllBoards(Pageable pageable) {
Page<Board> boards = boardRepository.findAll(pageable);
return boards.map(BoardResponseDto::FindFromBoard);
}
findAll로 pageable을 가져와 놓고 왜 이번에는 Repository에 메서드를 추가하냐?
먼저 findAll은 한개의 인자만 받을수 있게 설정되어 있다.
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
/**
* Returns all entities sorted by the given options.
*
* @param sort the {@link Sort} specification to sort the results by, can be {@link Sort#unsorted()}, must not be
* {@literal null}.
* @return all entities sorted by the given options
*/
Iterable<T> findAll(Sort sort);
/**
* Returns a {@link Page} of entities meeting the paging restriction provided in the {@link Pageable} object.
*
* @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
* {@literal null}.
* @return a page of entities
*/
Page<T> findAll(Pageable pageable);
}
그리고 board에 연관 되어 있는 pageable만 가져와야 하기에 메서드를 따로 만들어 줘야 하는것이다.
이쯤 설명하면 이해가 될것 이라 생각한다.
마지막으로 Controller인데 설명은 따로 필요 없다고 생각한다.
@GetMapping("{boardId}")
public ResponseEntity<Page<ReplyResponseDto>> getAllReply(
@PathVariable("boardId") Long boardId,
@RequestParam(value = "page",defaultValue = "1")int page,
@RequestParam(value = "size",defaultValue = "5")int size) {
Pageable pageable = PageRequest.of(page - 1, size);
Page<ReplyResponseDto> replies = replyService.findAllReply(pageable,boardId);
return ResponseEntity.status(HttpStatus.OK).body(replies);
}
다만, 우리는 boardId에 따라 거기어 물려있는 댓글들을 가져올꺼라 @PathVariable보면 이해하기 쉽게 boardId라 적어주자.
결과는?
1번 게시판의 1번 페이지 댓글 목록
1번 게시판의 2번 페이지 댓글 목록
2번 게시판의 1번 페이지 댓글 목록
2번 게시판의 2번 페이지 댓글 목록
DB Table
이렇게 댓글을 완성해 보았다.
다음은 회원가입 기능을 만들어 보겠다.
'스프링boot > 정리' 카테고리의 다른 글
Spring MVC) 사용자를 만들어 보자 2 (0) | 2023.04.21 |
---|---|
Spring MVC) 사용자를 만들어 보자 1 (0) | 2023.04.18 |
Spring MVC) 게시판을 만들어 보자 4 (0) | 2023.04.07 |
Spring MVC) 게시판을 만들어 보자 3 (0) | 2023.04.07 |
Spring MVC) 게시판을 만들어 보자 2 (1) | 2023.04.05 |