[JPA] 양방향 매핑 시 순환참조 문제

Coastby·2022년 11월 24일
0

문제 해결

목록 보기
5/17

🚫 에러 상황

병원 리뷰를 조회하는 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();
    }

}
profile
훈이야 화이팅

0개의 댓글