🙏내용에 대한 피드백은 언제나 환영입니다!!🙏
@Transactional(readOnly = true)
public Page<Post> postList(Pageable pageable) {
return postRepository.findAll(pageable);
}
@Transactional(readOnly = true)
public Page<Post> postSearchList(String searchKeyword, Pageable pageable) {
return postRepository.findByTitleContaining(searchKeyword, pageable);
}
위와 같은 코드를 아래의 @RestController에 작성하여 postman에 실행해 보았다.
@GetMapping("/post")
public Page<Post> findAll(@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
@RequestParam(required = false) String searchKeyword) {
if(searchKeyword == null)
return postService.postList(pageable);
else {
return postService.postSearchList(searchKeyword, pageable);
}
}
결과는 아래와 같은 오류 발생.
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
java.lang.StackOverflowError: null
이 오류에 대해서 알아보니 양방향 매핑으로 생긴 무한로프 문제였다.
@OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
@JsonIgnore
private List<Post> posts;
위와 같이 @JsonIgnore 어노테이션을 붙여 posts를 Json으로 직렬화되지 않게 하는 것이다. 하지만, 이것의 문제점은 후에 필요할 때(User가 작성한 Post들을 가져오는 경우를 가정), Json으로 불러오지 못한다는 단점이 있다.
이러한 문제점을 해결해주는 @JsonIgnoreProperties 어노테이션이 있지만, 이것 또한 특정 필드의 Json 직렬화를 막는 것이지, 문제점은 계속 발생한다.
먼저 설명하기 전, DTO에 대해서 알아보고 가겠다.
DTO(Data Transfer Object)란 데이터 전송 객체로, 데이터를 객체로 캡슐화하여 데이터 교환을 하는데 사용되는 디자인 패턴이다.
DTO를 사용하는 이유는 SoC(Separation of Concerns). 즉, 관심사의 분리이다.
도메인은 데이터베이스와 직접적으로 관련되어 있다. 그래서, 초기 설계 후 되도록이면 수정하지 않는 것이 안전하다.
하지만, View에서 필요로 하는 정보는 요구 사항에 따라 달라질 수 있다. 그리고, 비밀번호 같은 보안적인 내용 또한 담기면 안된다.
이것을 보면 도메인과 View에서의 관심사가 달라 생긴 것이다.
그래서, DTO를 통해서 필요한 데이터들만 모아 객체로 만들어 불필요한 정보는 제외하여 안전하고 효율적으로 사용하는 것이다.
( 이름과 역할을 보면 알수 있듯이 DTO의 관심사는 '데이터 전달'이다. )
다시 본론으로 와서, DTO를 사용하여 무한 참조 또한 막을 수 있다.
@Transactional(readOnly = true)
public Page<PostDTO.Response> postList(Pageable pageable) {
Page<Post> posts = postRepository.findAll(pageable);
return posts.map(PostDTO.Response::new);
}
@Transactional(readOnly = true)
public Page<PostDTO.Response> postSearchList(String searchKeyword, Pageable pageable) {
Page<Post> posts = postRepository.findByTitleContaining(searchKeyword, pageable);
return posts.map(PostDTO.Response::new);
}
위와 같이 DTO로 데이터 전달 객체를 바꾸고, 아래와 같이 @RestController에서도 DTO로 데이터를 받도록 수정하였다.
@GetMapping("/post")
public Page<PostDTO.Response> findAll(@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
@RequestParam(required = false) String searchKeyword) {
if(searchKeyword == null)
return postService.postList(pageable);
else {
return postService.postSearchList(searchKeyword, pageable);
}
}
첫번째 해결방법인 @JsonIgnore보다는 DTO를 사용하는 것이 후에 데이터를 가져오는 것과 SoC(관심사 분리)측면에서 생각해보면 더욱 효율적이다.
DTO를 전부터 사용하였지만, 이 문제가 생기기 전까지는 DTO의 중요성에 대해서 제대로 깨닫지 못하였다.
그냥 단순히 데이터를 비밀번호 같은 보안적인 문제를 전달하지 않는 역할으로만 사용하였다.
무한 참조 문제를 직면하고나서야 SoC(관심사 분리)에 대해서의 중요성과 DTO의 정확한 역할을 알게 되었다.