스프링부트 강좌 67강(블로그 프로젝트) - 댓글 목록 뿌리기
상세보기를 했을 때 댓글이 튀어나와야 하니까 boardService에서 작성해야 한다. 일단 레파지토리에 인터페이스를 하나 만든다. extends해서 JPA Prepositoyr로 등록한다.
package com.yuri.blog.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.yuri.blog.model.Reply;
public interface ReplyRepository extends JpaRepository<Reply, Integer>{
}
티입은 Reply 이고 reply의 id는 integer이니 integer로 한다.
boardService에서 글 상세보기를 할때 return 해야 하는 데이터가
Board.java 가 reply를 들고 있다. board를 select 하게 되면 즉 findbyId 해서 찾게 되면 id, title, count 등을 db에서 들고 온다.
Board.java
@OneToMany(mappedBy = "board", fetch = FetchType.EAGER) //mappedBy 연관관계의 주인이 아니다. (난 FK가 아니다). DB에 컬럼을 만들지 마시오.
private List<Reply> reply;
mappedBy 라고 붙어 있으니까 연관관계의 주인이 아니라는 뜻이다. 그 말을 db에는 없다는 뜻이다. db에는 없는데 board를 셀렉트할 때, eager 전략이니까 reply를 바로 패치해서 들고올 것이다.
BoardService.java
@Transactional(readOnly = true)
public Board 글상세보기(int id) {
return boardRepository.findById(id)
.orElseThrow(()->{
return new IllegalArgumentException("글 상세보기 실패 : 아이디를 찾을 수 없습니다.");
});
}
한마디로 BoardService에서 findById Board를 들고 오면 boardcontroller에서
BoardController.java
@GetMapping("/board/{id}")
public String findById(@PathVariable int id, Model model) {
model.addAttribute("board", boardService.글상세보기(id));
return "board/detail";
}
board를 return 하는데 board 오브젝트는 reply를 들고 있다. detail로 가면 board는 reply를 들고 있으니까 여기다가 바로 for-each만 적으면 된다. 이게 바로 jpa의 매력이다.
쉽게 이야기 하면 내가 만약 Board를 셀렉트 하면 reply를 가져오고 user도 들고온다. 유저를 들고 오면 유저 정보(board를 쓴 작성자)를 쫙 들고 올 수 있겠고 reply를 들고 오면 board, user를 또 들고온다. 이때 user는 reply를 적은 작성자이다.
무한 참조가 일어난다. 그래서 하나만 추가를 할 것이다.
지금은 상관없지만..무한참조라는 문제가 발생할 수 있다.
ReplyControllerTest.java
package com.yuri.blog.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.yuri.blog.model.Board;
import com.yuri.blog.repository.BoardRepository;
@RestController
public class ReplyControllerTest {
@Autowired
private BoardRepository boardRepository;
@GetMapping("/test/board/{id}")
public Board getBoard(@PathVariable int id) {
return boardRepository.findById(id).get();
}
}
localhost:8000/test/board/2 를 요청한 결과
미친듯이 무한참조를 한다. 왜 그런 것일까?
나는 board 만 가져오면 된다. 근데 board를 가져오기 위해서는 User 정보와 replys를 가져오게 되어있다. JPA가 연관되어있는 것들을 자동으로 조인해서 들고와주기 때문이다.
User를 가져올 때는 아무런 연관관계가 없기 때문에 깔끔하게 들고 올 것이다. 근데 replys가 문제이다. replys 가 무엇이 문제냐면..... return 할 때
모델이 들고 있는 getter를 호출해서 뽑아내 json으로 바꿔준다. boardRepository.findById(id).get(); 이게 board 오브젝트이니까 얘가 실행된다는 것은
id, title, content, count, user, replys, createDate 가 호출된다는 것이다. 그럼 get.user을 호출하면
User 내부에서도 json으로 바꿔야 하니까 get.id, get.username, get password, get.email, get.role, get oauth, get.createDate가 호출된다. 여기까지는 전혀 문제가 없다. 근데 get.replys를 리턴 할때 문제가 발생한다. 왜냐하면
reply 안에 get.id, get.content, get.board(!!!) 다시 get.board를 리턴하게 된다. 그래서 다시 board 안에서 id, title, content, count, user, replys, createDate 를 또 리턴한다. 미친듯이 ...반복된다.
이 문제를 간단하게 해결하기 위해서는 최처에 get.board를 요청했을 때 전부다 잘 호출하고 다시 reply안에서 다시 board를 호출하지 않게 하면 된다. 방법이 여러가지 있지만 지금은 한가지만 해보자.
(1) Entity로 받고 Json직렬화 하기 전에 DTO 생성후 복사하기
BeanUtils.copyProperties(A,B)
(2) 처음부터 DTO로 DB에서 받기
(3) @JsonIgnore
(4) @JsonIgnoreProperties({"board"})
(5) @JsonBackReference @JsonManagedReference
@JsonIgnore, @JsonIgnoreProperties({"board"})를 배워보자.
Board.java
@OneToMany(mappedBy = "board", fetch = FetchType.EAGER) //mappedBy 연관관계의 주인이 아니다. (난 FK가 아니다). DB에 컬럼을 만들지 마시오.
@JsonIgnoreProperties({"board"})
private List<Reply> replys;
이 안에서 JsonIgnoreProperties를 걸어주면 reply 안에서 호출을 또 할게 될때 board는 getter 호출이 안된다. 무시가 된다.
다시 똑같은 요청으로 테스트를 한 결과이다. 무한참조 되지 않은 모습을 볼 수 있다.
보다시피 board를 호출하게 되면 알아서 JPA가 다 뽑아준다. user정보까지 뽑아준다. (eager 전략이기 때문에)
replys 역시 eager 전략이라 바로 뽑아준다. 자세히 보면 board가 없다. 내가 JsonIgnoreProperties 를 설정해주었기 때문이다. 이 방법 말고도 많은 방법들이 있다. 마지막쯤에 수업을 할 예정이다.
user도 추가해봄.
Board.java
@OneToMany(mappedBy = "board", fetch = FetchType.EAGER) //mappedBy 연관관계의 주인이 아니다. (난 FK가 아니다). DB에 컬럼을 만들지 마시오.
@JsonIgnoreProperties({"board", "user"})
private List<Reply> replys;
ReplyControllerTest.java
package com.yuri.blog.test;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.yuri.blog.model.Board;
import com.yuri.blog.model.Reply;
import com.yuri.blog.repository.BoardRepository;
import com.yuri.blog.repository.ReplyRepository;
@RestController
public class ReplyControllerTest {
@Autowired
private BoardRepository boardRepository;
@Autowired
private ReplyRepository replyRepository;
@GetMapping("/test/board/{id}")
public Board getBoard(@PathVariable int id) {
return boardRepository.findById(id).get(); //jackson 라이브러리가 발동함(오브젝트를 json으로 리턴) => 모델이 getter를 호출
}
@GetMapping("/test/reply")
public List<Reply> getReply() {
return replyRepository.findAll(); //jackson 라이브러리가 발동함(오브젝트를 json으로 리턴) => 모델이 getter를 호출
}
}
그리고 reply 를 모두 뽑아봄 test
board 와 user을 가져옴 왜냐면 reply를 direct로 호출했기 때문에 ..
board 안에 또 reply가 있음 , 그 reply 안에는 board와 user가 없다.
이렇게만 해도 무한참조를 방지할 수 있다.
boardservice.java
@Transactional(readOnly = true)
public Board 글상세보기(int id) {
return boardRepository.findById(id)
.orElseThrow(()->{
return new IllegalArgumentException("글 상세보기 실패 : 아이디를 찾을 수 없습니다.");
});
}
우리가 지금 하는 것은 boardservice에서 글 상세보기를 했을 때 getter 호출이 안된다. 내가 get 호출을 따로 하지 않는 이상....~~~
-이 글은 유투버 겟인데어의 스프링 부트 강좌를 바탕으로 정리한 내용입니다.-