1. 문제
간단한 게시판 프로젝트 중 게시글 조회에서 오류가 발생하였다.
// User Entity
@Entity
@Table(name = "users")
@AllArgsConstructor
@Getter
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@OneToMany(mappedBy = "user")
private List<Post> posts = new ArrayList<>();
}
//Post Entity
@Entity
@Table(name = "posts")
@AllArgsConstructor
@Getter
@NoArgsConstructor
public class Post extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false ,length = 500)
private String title;
@Column(nullable = false, length = 5000)
private String content;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
}
//PostController
@GetMapping("/{postId}")
public ResponseEntity<PostResponseDto> getPost(@PathVariable Long postId) throws Exception {
return ResponseEntity.ok(postService.getPost(postId));
}
//PostService
public PostResponseDto getPost(Long postId) throws Exception {
Post post = postRepository.findById(postId).orElseThrow( () ->
new Exception("게시글이 존재하지 않습니다."));
PostResponseDto postResponseDto = new PostResponseDto(post);
System.out.println(postResponseDto);
return postResponseDto;
}
// PostResponseDto
@Getter
@NoArgsConstructor
public class PostResponseDto {
private String username;
private String title;
private String content;
private User user;
public PostResponseDto(Post post) {
this.username = post.getUser().getUsername();
this.title = post.getTitle();
this.content = post.getContent();
this.user = post.getUser();
}
}
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
at org.apache.catalina.connector.ResponseFacade.checkCommitted(ResponseFacade.java:485) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:337) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
...
User Entity와 Post Entity가 서로 OneToMany, ManyToOne으로 양방향 매핑이 되어 있을 때 Post를 조회하면
Post안에 User(Post를 작성한 유저)가 있고 또한 User안에 Post List(User가 작성한 Poste들)가 있기 때문에 순환 참조하게 되는 문제가 생겼다.
쉽게 말해 서로가 서로를 무한 순환 참조하여 stackoverflowr가 발생한다.
* 직렬화란?
객체의 내용을 바이트 단위로 변환하여 파일 또는 네트워크를 통해 스트림(송수신)하도록 한는 것을 의미한다.
2. 해결방법
1) @JsonIgnore
//Post Entity
@Entity
@Table(name = "posts")
@AllArgsConstructor
@Getter
@NoArgsConstructor
public class Post extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false ,length = 500)
private String title;
@Column(nullable = false, length = 5000)
private String content;
@ManyToOne
@JsonIgnore // 추가!
@JoinColumn(name = "user_id", nullable = false)
private User user;
}
첫 번째 방법은 Post Entity안 User 필드에 @JsonIgnore을 추가하는 방법이 있다.
이 어노테이션은 해당 어노테이션이 달린 필드에 대해 직렬화하지 않게 해주는 기능이다.
2) @JsonManagedReference & @JsonBackReference
양방향 관계에서 직렬화 방향을 설정해주어 문제를 해결해 주는 기능을 한다.
@JsonManagedReference는 연관관계 주인의 반대 Entity에 선언해주어 정상적으로 직렬화를 수행하게 해 주고
@JsonBackReference는 주인 Entity에 선언해주어 직렬화가 되지 않도록 수행한다.
3) Entity가 아닌 DTO 이용
나의 경우는 PostResponseDto에 User 객체를 그대로 넣어주어 순환 참조 오류가 발생하게 되었다. 따라서 이를 User 객체 대신 UserDto를 만들어 준다면 순환 참조 오류를 방지할 수 있으며 다음에 같은 문제가 생긴다면 이 방법을 선택할 것 같다.
3. 결론
이를 통해 DTO를 사용해야 하는 이유를 한 번 더 되새기게 되었으며 직렬화의 뜻이 뭔지, 또한 만약 양방향 매핑이 무조건 필요한 게 아니라면 단방향 매핑도 고려해 봐야겠다는 생각을 하게 되었다.
참고 블로그
https://wildeveloperetrain.tistory.com/265