controller 리턴타입을 엔티티로 했을 때 생기는 일

no Session, ByteBuddyInterceptor, 무한루프... DTO 를 쓰러 가야겠다는 생각뿐.
HootJem's avatar
Sep 05, 2024
controller 리턴타입을 엔티티로 했을 때 생기는 일
 
컨트롤러에 @ResponseBody 를 붙여 리턴하게 되면 Json 데이터 형식으로 값을 리턴한다.
값을 전달할 때 사용되는 객체는 DTO 인데
왜 엔티티클래스를 사용하지 않는것일까? (애초에 엔티티는 데이터 전달에 사용되는 객체가 아니지만...)
 
엔티티클래스를 컨트롤러에서 리턴하면 생기는 일을 알아보자.

 

1. spring.jpa.open-in-view=false일때

간단히 설명하자면 JPA 가 데이터베이스 커넥션을 언제 반납할 지 결정하는 설정이다.
 
public class Board { @GeneratedValue(strategy = GenerationType.IDENTITY) @Id private Integer id; @Column(nullable = false) private String title; @Column(nullable = false) private String content; @CreationTimestamp private Timestamp createdAt; @ManyToOne(fetch = FetchType.LAZY) private User user; @OneToMany(mappedBy = "board")//반대 방향에도 거는게 양방향 매핑 private List<Reply> replies; @Builder public Board(Integer id, String title, String content, Timestamp createdAt, User user) { this.id = id; this.title = title; this.content = content; this.createdAt = createdAt; this.user = user; } }
Board 엔티티가 이렇게 존재하고
@GetMapping("/v1/board/{id}/update-form") public @ResponseBody Board updateForm1(@PathVariable("id") int id, HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); Board board = boardService.게시글수정화면(id, sessionUser); return board;
 
컨트롤러의 리턴을 엔티티로 설정한다.
이렇게 하면
notion image
저 부분에서 에러가 발생하게 된다.
notion image
opne-in-view 를 false 로 설정해 놓았기 때문인데
이미 게시글수정화면 서비스에서 데이터를 다 조회했는데 어째서 영속성 컨텍스트나, opne-in-view 나 session 이 연관이 되어있냐 하면
 
  1. 게시글 수정화면에서 findById 를 실행한다 (user는 LAZY 전략으로 인해 가져오지 않음)
  1. 컨트롤러가 json 형식으로 데이터를 변환한다.(ResponseBody 때문에)
  1. private User user; 차례가 되면 컨트롤러는 null 값을 마주하게 되고, 하이버네이트는 get 요청을 한 줄 알고 값을 찾으러간다.
  1. 그러나 이미 데이터베이스 커넥션은 반납되었기 때문에 session 을 찾을 수 없다.
 
그럼 데이터베이스 커넥션을 true 로 바꿔서 연결하게 해주자.
 

2. ByteBuddyInterceptor

현재 상황. opne-in-view 를 true 로 변경한다. → controller 에서도 db에 접근이 가능해짐.
 
notion image
 
ExceptionHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]]
분명 controller 에서도 접근이 가능하도록 설정을 변경 했는데도 에러가 뜬다.
ByteBuddyInterceptor 는 LAZY 로딩과 관련이 있다.
엔티티를 Json 데이터로 만드는 도중 아까처럼 User 을 만나게되면 값을 조회하러 db 로 달려가지만, 이미 비어있는 객체를 Serialize 하기 때문이다.
 
무슨 설정 방법이 있다는데 그냥 DTO 를 쓰는게 제일 간단하다.

 

3. 무한 루프

지금 양방향 매핑된 엔티티가 있다.
notion image
notion image
  • 컨트롤러
@GetMapping("/v3/board/{id}") public @ResponseBody Board detailV3(@PathVariable("id") Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); Board model = boardService.게시글상세보기V3(sessionUser, id); return model; }
  • 서비스
public Board 게시글상세보기V3(User sessionUser, Integer boardId){ Board boardPS = boardRepository.mFindByIdWithReply(boardId) .orElseThrow(() -> new Exception404("게시글이 없습니다.")); return boardPS; }
  • 레포지토리
    • 보드와 유저이름, 댓글을 조인하는 쿼리이다.
@Query("select b from Board b join fetch b.user left join fetch b.replies r left join fetch r.user where b.id=:id") Optional<Board> mFindByIdWithReply(@Param("id")int id);
 
이 코드를 사용하여 엔티티 클래스에서 json 변환을 하면 이렇게 된다.
notion image
큰 네모는 괜찮지만 replies 부터 비극이 시작된다. 양방향 매핑으로 인해
replies → board → replies → board → … 서로를 호출하기 시작한다. 영원히.
 
이때 사용하는 것이
@JsonIgnoreProperties 어노테이션이다 Json 으로 변환할 때 제외할 것을 적는 것인데
board 엔티티의 replies 에 추가를 한 뒤 다시 실행해보자.
@JsonIgnoreProperties({"board"}) // json 데이터로 만들러 가지마라 @OneToMany(mappedBy = "board")//반대 방향에도 거는게 양방향 매핑 private List<Reply> replies;
{ "id": 5, "title": "제목5", "content": "내용5", "createdAt": "2024-09-05T05:32:27.013+00:00", "user": { "id": 2, "username": "cos", "password": "1234", "email": "cos@nate.com", "createdAt": "2024-09-05T05:32:27.011+00:00" }, "replies": [ { "id": 1, "comment": "댓글1", "user": { "id": 1, "username": "ssar", "password": "1234", "email": "ssar@nate.com", "createdAt": "2024-09-05T05:32:27.010+00:00" }, "createdAt": "2024-09-05T05:32:27.013+00:00" }, { "id": 2, "comment": "댓글2", "user": { "id": 1, "username": "ssar", "password": "1234", "email": "ssar@nate.com", "createdAt": "2024-09-05T05:32:27.010+00:00" }, "createdAt": "2024-09-05T05:32:27.013+00:00" }, { "id": 3, "comment": "댓글3", "user": { "id": 2, "username": "cos", "password": "1234", "email": "cos@nate.com", "createdAt": "2024-09-05T05:32:27.011+00:00" }, "createdAt": "2024-09-05T05:32:27.013+00:00" } ] }
repliesboard를 호출하지 않아 무사히 데이터가 출력되었다.
 
하지만 굳이 게시글 상세보기를 위해 글쓴이와 댓글작성자의 id, password, email… 모든걸 가져올 필요는 없다.
따라서 DTO를 만들어서 해결하는 것이 좋다.
 
다음 포스팅에선(조회된) 데이터 관리를 위한 DTO 를 활용하는 방법을 알아보자.
 
Share article

[HootJem] 개발 기록 블로그