병원 리뷰를 조회하는 API를 만드는 중, JPA를 이용하여 병원과 리뷰를 @OneToMany, @ManyToOne으로 양방향 매핑을 하였다.
⌨️ Controller
@GetMapping(value = "/{id}")
public ResponseEntity<List<Comment>> showHospital(@PathVariable Long id){
HospitalResponse hospitalResponse = hospitalService.getHospital(id);
List<Comment> result = hospitalResponse.getCommentList();
return ResponseEntity.ok().body(result);
}
}
⌨️ Comment
@Entity
@ToString
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
private String user;
@ManyToOne
@JoinColumn(name = "hospital_id")
private Hospital hospital;
}
⌨️ Hospital
@Entity
@Getter
@NoArgsConstructor
public class Hospital {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String hospitalName;
private String roadNameAddress;
@OneToMany(mappedBy="hospital", fetch = FetchType.LAZY)
@ToString.Exclude
private List<Comment> commentList = new ArrayList<>();
}
그리고 조회를 하자 에러메세지가 어마어마하게 떴다.
StackOverFlowError
: 에러메시지만 보면 그 유명한 StackOverFlow가 발생하였다. 대략 에러내용을 보면 HttpMessageNotWritableException
, JsonMappingException
이 발생했으며 이는 reference chain(순환 참조)에 의해 무한 재귀가 발생했기 때문이다.
JSON을 작성하면서 Hospital의 CommentList를 불러오고 Comment에서 Hospital을 불러오고 하면서 발생한 듯 하다.
보통 REST API 방식에서 JSON 형태로 데이터를 주고받는다.
그리고 스프링부트를 이용한 프로젝트에서는 ‘spring-boot-starter-web’을 의존성에 추가하면 Jackson이라는 라이브러리가 자동으로 추가된다. Jackson의 ObjectMapper 클래스는 JSON을 객체로 (deserialization), 객체를 JSON으로 (serialization) 매핑해준다.
이 ObjectMapper를 사용한 MappingJackson2HttpMessageConverter
가 @ResponseBody
, @RequestBody
, ResponseEntity<>
와 같은 곳에 사용되면서 JSON ↔ 객체 간의 매핑이 이루어진다.
이 때 상호 참조 관계의 양방향 도메인 모델들을 Jackson을 이용해 JSON 직렬화를 시도하면 관계가 타고 타고 가다가 StackOverFlow가 발생한다.
✅ 일단 양방향 매핑을 제거해보았다.
양방향을 설정하지 않아도 한 병원의 리뷰를 가져오려면 CommentRepository에 Spring Data JPA를 이용하여 메소드를 추가해주어도 원하는 기능 (댓글 보여주기)은 구현할 수 있었다.
그리고 양방향으로 매핑을 하게되면 순환참조를 피하기 위해 DTO로 변환하는 과정을 거쳐야할 것 같아 굳이?란 생각이 들긴하였다. Comment의 리스트를 json으로 바꿀 때는 항상 DTO로 변환해야 하기 때문이다.
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findAllByHospitalId(Long id);
}
✅ 그러나 양방향 매핑을 연습해야 하니
HospitalResponse의 commentList를 List로 바꾸어주었다. 그래서 response를 만들 때 stream을 사용해서 Comment → CommentResponse로 전환하는 로직을 추가하였다.
찾아보니 DTO를 사용하는 방법 외에도 JSON 어노테이션을 이용하는 방법들도 있었다. 다양한 어노테이션이 있어 상황에 따라 사용하면 좋겠지만 직렬화가 필요없는 상황에 유용할 것 같다. (그러나 양방향 매핑을 한 이유가 있을텐데..?)
참고 : Jackson 양방향 관계 모델의 순환 참조 피하기
@Getter
@Builder
public class HospitalResponse {
private Long id;
private String hospitalName;
private String roadNameAddress;
private List<CommentResponse> commentList;
public static HospitalResponse of(Hospital hospital){
//List<Comment> -> List<CommentResponse>
List<CommentResponse> commentResponses = hospital.getCommentList()
.stream().map(CommentResponse::of).collect(Collectors.toList());
return HospitalResponse.builder()
.id(hospital.getId())
.hospitalName(hospital.getHospitalName())
.roadNameAddress(hospital.getRoadNameAddress())
.commentList(commentResponses)
.build();
}
}