댓글 디자인
detail.jsp
<div class="card">
<div class="card-body">
<textarea class="form-control" rows="1"></textarea>
</div>
<div class="card-footer">
<button class="btn btn-primary">등록</button>
</div>
</div>
<br>
<div class="card">
<div class="card-header">댓글 리스트</div>
<ul id="reply--box" class="list-group">
<li id="reply--1" class="list-group-item d-flex justify-content-between">
<div>댓글 내용입니다!!</div>
<div class="d-flex">
<div class="font-italic">작성자 : cos1234 </div>
<button class="badge">삭제</button>
</div>
</li>
</ul>
</div>
댓글 목록 뿌리기
샘플 데이터 생성
ReplyRepository.java
package com.cos.blog.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.cos.blog.model.Reply;
public interface ReplyRepository extends JpaRepository<Reply, Integer>{
}
detail.jsp
<div class="card">
<div class="card-header">댓글 리스트</div>
<ul id="reply--box" class="list-group">
<c:forEach var="reply" items="${board.replys}">
<li id="reply--1" class="list-group-item d-flex justify-content-between">
<div>${reply.content}</div>
<div class="d-flex">
<div class="font-italic">${reply.user.username} </div>
<button class="badge">삭제</button>
</div>
</li>
</c:forEach>
</ul>
</div>
그런데 이렇게 하면 StackOverflowError 에러가 발생한다. 이유는 board를 부르면 user와 replys가 딸려오는데 replys가 딸려올 때 또 board와 user를 가져와서 무한 참조가 발생하여 스택 오버 플로우 에러가 발생하는 것이다.
오류의 원인을 알 기 위해 테스트를 해보자
ReplyControllerTest.java
@RestController
public class ReplyControllerTest {
@Autowired
private BoardRepository boardRepository;
@GetMapping("/test/board/{id}")
public Board getBoard(@PathVariable int id) {
return boardRepository.findById(id).get();
// jackson 라이브러리 발동 (오브젝트를 json으로 리턴) => 모델의 getter 호출
}
}
무한 참조가 일어나는 것을 볼 수 있다.
boardRepository.findById(id).get(); 로 board를 id로 조회하여 가져올 때 User는 아무런 연관관계가 없으므로 상관없는데 Reply가 문제다. 이 구문이 실행되면 jackson 라이브러리 발동되면서 오브젝트를 json으로 리턴시킨다. 이 때 모델의 getter를 호출하여 json으로 바꿔주는 원리이다. 이 때 Reply와 연관관계인 board와 user를 또 return하기 때문에 무한 참조 사이클에 걸려버리는 것이다.
Board로 replys를 호출할 때 연관된 board만 호출시켜주지 않으면 무한 참조를 막을 수 있다.
방법은 여러가지이지만 다음 방법 하나만 일단 익혀보자.
@JsonIgnoreProperties({"board"}) 이용하기
Board.java
// Reply 테이블의 필드명 board
@OneToMany(mappedBy = "board", fetch = FetchType.EAGER)
@JsonIgnoreProperties({"board"})
private List<Reply> replys;
무한 참조가 막힌 것을 볼 수 있다. 이 때 user 정보까지 전부 뽑히는 것을 볼 수 있는데 이유는 JPA가 EAGER 전략으로 유저 정보를 뽑아오기 때문이다.
강의에서는 이렇게만 하면 무한참조 에러가 해결되었다. 근데 내가 해보니 계속 똑같은 에러가 발생했다. 테스트는 성공적으로 돌아가지만 본 코드에서는 그대로 에러였다.. sysout으로 로그가 찍히게 되면 reply를 통해 무한참조가 발생할 수도 있다는 댓글을 보고 sysout 관련 코드를 전부 주석처리 해봤지만 그대로였다..
결국 강의자님께 질문드려서 해결했다.. 문제는 @Data 어노테이션이었다. 에러를 보면 toString이 무한참조 되고 있었는데 @Data 어노테이션 안에 @toString이 포함되어 있었기 때문이었다..!
Board와 User의 @Data를 @getter, @setter로 바꿔서 실행해주면 샘플댓글이 정상적으로 등록된 걸 볼 수 있다..!😭
@Data 어노테이션 = @toString + @getter + @setter + @RequiredArgsConstructor + @EqualsAndHashCode
@toString : toString 메서드 생성.
@getter/@setter : getter() setter() 메서드 생성
@RequiredArgsConstructor : 초기화되지 않은 모든 final 필드, @NonNull과 같이 제약조건이 설정되어 있는 모든 필드들에 대한 생성자 자동 생성.
@EqualsAndHashCode : equals(), hashCode() 메서드 생성
댓글 작성하기
먼저 댓글 리스트를 최신순으로 정렬해주자. 간단하게 @OrderBy 어노테이션을 통해 id desc 즉, id값 기준 내림차순 정렬을 해주면 된다. 댓글의 id값은 최신에 적힌 글일수록 높기 때문이다.
Board.java
@OneToMany(mappedBy = "board", fetch = FetchType.EAGER)
@JsonIgnoreProperties({"reply"})
@OrderBy("id desc")
private List<Reply> replys;
댓글 쓰기
먼저 detail.jsp에서 댓글 쓰는 텍스트 창과 등록 버튼을 form으로 감싸주고 id를 부여한다. 또 각 댓글의 식별을 위해 hidden 값으로 boardId를 넣어준다. 또한 form안에 있으면 button은 summit이 돼버리니까 type으로 버튼임을 명시해주자.
detail.jsp
<div class="card">
<form>
<input type="hidden" id="boardId" value="${board.id}">
<div class="card-body">
<textarea id="reply-content" id="reply-content" class="form-control" rows="1"></textarea>
</div>
<div class="card-footer">
<button id="btn-reply-save" type="button" id="btn-reply-save" class="btn btn-primary">등록</button>
</div>
</form>
</div>
그 후 board.js에서 등록 버튼의 id값인 btn-reply-save로 함수를 작성한다.
board.js
let index = {
init: function() {
...
$("#btn-reply-save").on("click", () => {
this.replySave();
});
},
...
replySave: function() {
let data = {
boardId: $("#boardId").val(),
content: $("#reply-content").val()
};
console.log(data)
$.ajax({
type: "POST",
url: `/api/board/${data.boardId}/reply`,
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
dataType: "json"
}).done(function(resp) {
alert("댓글 작성이 완료되었습니다.");
location.href = `/board/${data.boardId}`;
}).fail(function(error) {
alert(JSON.stringify(error));
});
},
}
index.init();
이제 /api/board/{boardId}/reply에 대한 api를 작성한다.
BoardApiController.java
@PostMapping("/api/board/{boardId}/reply")
public ResponseDto<Integer> replySave(@PathVariable int boardId, @RequestBody Reply reply, @AuthenticationPrincipal PrincipalDetail principal) {
boardService.댓글쓰기(principal.getUser(), boardId, reply);
return new ResponseDto<Integer>(HttpStatus.OK.value(), 1);
}
댓글쓰기 Service 작성
BoardService.java
@Transactional
public void 댓글쓰기(User user, int boardId, Reply requestReply) {
Board board = boardRepository.findById(boardId)
.orElseThrow(()->{
return new IllegalArgumentException("댓글 작성 실패 : 게시글 id를 찾을 수 없습니다.");
});
requestReply.setUser(user);
requestReply.setBoard(board);
replyRepository.save(requestReply);
}
사실 여기서는 작은 프로젝트이기도 하고 필드값이 몇 개 안되니까 model로 받아왔는데 큰 프로젝트는 이런식으로 하면 안된다. 그 많은 필드들을 하나하나 model로 받으면 지저분해질 것이다. 따라서 dto를 만들어서 받아줘야 한다.
ReplySavaRequestDto.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ReplySaveRequestDto {
private int userId;
private int boardId;
private String content;
}
BoardApiController.java
@PostMapping("/api/board/{boardId}/reply")
public ResponseDto<Integer> replySave(@RequestBody ReplySaveRequestDto replySaveRequestDto) {
boardService.댓글쓰기(replySaveRequestDto);
return new ResponseDto<Integer>(HttpStatus.OK.value(), 1);
}
사용자 정보는 detail.jsp에서 form안에 hidden 값으로 받아오면 된다.
detail.jsp
<input type="hidden" id="userId" value="${principal.user.id}" />
board.js
replySave: function() {
let data = {
userId: $("#userId").val(),
boardId: $("#boardId").val(),
content: $("#reply-content").val()
};
...
},
BoardService.java
@Transactional
public void 댓글쓰기(ReplySaveRequestDto replySaveRequestDto) {
User user = userRepository.findById(replySaveRequestDto.getUserId())
.orElseThrow(()->{
return new IllegalArgumentException("댓글 작성 실패 : 유저 id를 찾을 수 없습니다.");
});
Board board = boardRepository.findById(replySaveRequestDto.getBoardId())
.orElseThrow(()->{
return new IllegalArgumentException("댓글 작성 실패 : 게시글 id를 찾을 수 없습니다.");
});
Reply reply = Reply.builder()
.user(user)
.board(board)
.content(replySaveRequestDto.getContent())
.build();
replyRepository.save(reply);
}
이렇게 해도 되긴 되지만 보면은 또 필드가 많을 경우 Reply 객체를 만들 때 지저분해진다. 더 좋은 방법은 이 Reply 객체를 만드는 과정을 함수로 만들어버리는 것이다.
Reply.java
public void update(User user, Board board, String content) {
setUser(user);
setBoard(board);
setContent(content);
}
BoardService.java
@Transactional
public void 댓글쓰기(ReplySaveRequestDto replySaveRequestDto) {
...
Reply reply = new Reply();
reply.update(user, board, replySaveRequestDto.getContent());
replyRepository.save(reply);
}
댓글 작성 시 네이티브 쿼리 사용해보기
Reply 객체는 user와 board 오브젝트를 알아야 만들 수 있다. userId와 boardId를 findBy로 찾아서 오브젝트로 반환해서 넣어줬었다. 너무 귀찮은 방법이다..
ReplyRepository.java
public interface ReplyRepository extends JpaRepository<Reply, Integer>{
@Query(value="INSERT INTO reply(userId, boardId, content, createDate) VALUES(?1, ?2, ?3, now())", nativeQuery = true)
void mSave(int userId, int boardId, String content);
}
이렇게 네이티브 쿼리를 사용해서 함수를 작성해두면 이전처럼 영속화를 통해 오브젝트를 가져오는 귀찮은 작업을 할 필요가 없다.
BoardService.java
@Transactional
public void 댓글쓰기(ReplySaveRequestDto replySaveRequestDto) {
replyRepository.mSave(replySaveRequestDto.getUserId(), replySaveRequestDto.getBoardId(), replySaveRequestDto.getContent());
}
mSave만 호출해주면 끝!! 일 줄 알았으나.. 에러가 발생한다..
이유는 ReplyRepository에서 작성한 쿼리문을 SELECT 쿼리로 인식해서 ResultSet을 만드는데 void로 설정해줘서 발생하는 오류였다. @Modifying 어노테이션을 붙여주고 int로 받아주면 해결된다. 기본적으로 JDBC가 INSERT, UPDAET, DELECT 작업을 수행할 때 return 값을 업데이트된 행의 개수를 리턴해주기 때문에 int로 받아주는 것이다.
ReplyRepository.java
public interface ReplyRepository extends JpaRepository<Reply, Integer>{
@Modifying
@Query(value="INSERT INTO reply(userId, boardId, content, createDate) VALUES(?1, ?2, ?3, now())", nativeQuery = true)
int mSave(int userId, int boardId, String content);
}
@Autowired의 원리
@Autowird는 DI를 하는 어노테이션이다. DI가 되는 원리는 스프링이 new를 해서 기본 생성자를 호출해서 들고 가는 방식이다. 그런데 @Autowird도 큰 프로젝트에서는 많아지면 지저분해질 수 있다. 더 간결한 방법으로 바꿔보자
@Autowird없이 DI를 하고 싶을 경우 생성자의 파라미터를 통해 주입해주면 된다.
BoardService.java
@Service
public class BoardService {
private BoardRepository boardRepository;
private ReplyRepository replyRepository;
public BoardService(BoardRepository bRep, ReplyRepository rRepo) {
this.boardRepository = bRep;
this.replyRepository = rRepo;
}
...
}
이보다 더 간단한 방법이 있다. 생성자 오버라이딩 필요 없이 final을 통해 그냥 선언해주기만 하면 된다. 근데 final로 선언 시 초기화가 꼭 필요하다. 그래서 @RequiredArgsConstructor을 통해 초기화시켜준다.
@Service
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
private final ReplyRepository replyRepository;
...
}
댓글 삭제하기
onClick 옵션으로 버튼을 클릭시 이벤트 발생할 수 있도록 설정해준다. replyDelete 함수를 호출해서 삭제할 것이다.
detail.jsp
<div class="card">
<div class="card-header">댓글 리스트</div>
<ul id="reply-box" class="list-group">
<c:forEach var="reply" items="${board.replys}">
<li id="reply-${reply.id}" class="list-group-item d-flex justify-content-between">
<div>${reply.content}</div>
<div class="d-flex">
<div class="font-italic">${reply.user.username} </div>
<button onClick="index.replyDelete(${board.id}, ${reply.id})" class="badge">삭제</button>
</div>
</li>
</c:forEach>
</ul>
</div>
board.jsp
replyDelete: function(boardId, replyId) {
$.ajax({
type: "DELETE",
url: `/api/board/${boardId}/reply/${replyId}`,
dataType: "json"
}).done(function(resp) {
alert('댓글 삭제가 완료되었습니다.');
location.href = `/board/${boardId}`;
}).fail(function(error) {
alert(JSON.stringify(error));
});
},
re댓
BoardApiController.java
@DeleteMapping("/api/board/{boardId}/reply/{replyId}")
public ResponseDto<Integer> replyDelete(@PathVariable int replyId){
boardService.댓글삭제(replyId);
return new ResponseDto<Integer>(HttpStatus.OK.value(),1);
}
BoardService.java
@Transactional
public void 댓글삭제(int replyId) {
replyRepository.deleteById(replyId);
}
출처 : 메타코딩 https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9
'Spring > Blog 만들기 with SpringBoot' 카테고리의 다른 글
에러 수정 (0) | 2022.09.06 |
---|---|
카카오 로그인 구현 (0) | 2022.08.21 |
회원 수정 (0) | 2022.07.25 |
게시판 (0) | 2022.07.25 |
비밀번호 해쉬 & 시큐리티 로그인 (0) | 2022.07.24 |