[스프링/Spring] @OneToMany/@ManyToOne 연관관계에서의 Infinite Recursion 문제를 DTO로 해결하기

kindof

·

2021. 9. 28. 23:52

🧐 문제 상황

투표 기능 API를 만들다가 마주했던 문제인데요. 아래와 같이 Vote, VoteOption이라는 두 개의 도메인이 존재합니다.

 

Vote 도메인은 '투표'라는 엔티티를 의미하며 어떤 게시물(Post)에 종속되어 있고 여러 개의 투표 항목 옵션(VoteOption)과 일대다 관계로 매핑되어 있습니다. 하나의 투표에는 선택할 수 있는 투표항목이 여러 개 존재하기 때문이죠.

 

그리고 VoteOption은 한 개의 Vote에 여러 개가 존재하므로 @ManyToOne 어노테이션으로 Vote를 참조합니다.

Vote
VoteOption

이 상황에서 "어떤 게시물에 존재하는 투표를 조회하는" API를 작성하려고 합니다.

 

Request-Header로 조회한 유저의 아이디를 받고, 게시물의 아이디와 투표의 아이디를 PathVariable로 받는 RestController입니다.

findByPostId API

이 때 Service는 해당 정보를 받아서 게시물에 투표가 존재하는지 확인하고, 종료된 투표인지 확인합니다. 그리고 요구사항에 따라 투표를 조회한 유저가 해당 투표에 투표를 했는지 Setter를 통해 설정해주고, VoteDto객체를 리턴합니다.

Service

문제가 되는 지점은 VoteDto인데요. 우선 VoteDto를 한 번 살펴보겠습니다.

VoteDto - (1)

Service에서 게시물 아이디로 조회한 Vote 엔티티를 객체로 넘겨주어 VoteDto의 생성자로 사용합니다. 그리고 VoteDto라는 객체를 넘겨받아 JSON 형태로 리턴해주는 로직이죠.

 

하지만 위와 같은 방식으로 Dto를 만들어서 넘겨주면 아래와 같이 무한 참조로 인한 StackOverflowError가 발생했습니다.

ERROR

왜 그럴까요? 

 

여기서 VoteDto는 VoteOption을 리스트의 멤버로 가지고 있습니다. 그리고 컨트롤러에서 마지막에 리턴해주는 결과는 이 DTO였죠. 자, 그런데 프론트 딴에서 뷰를 작성하든, 어떤 로직을 짜든지 DTO는 JSON 형태로 바뀌어서 HTTP Response로 보내져야 합니다.

 

그리고 여기서 DTO를 JSON 형태로 바꿔주는 ObjectMapper(jackson)가 존재하는데요. 이 ObjectMapper가 VoteOption을 JSON으로 바꾸려고 봤더니 거기에 Vote가 있고, Vote를 JSON으로 바꿔주려니까 voteOption이 서로 연관관계를 맺고 있던 것이죠. 그래서 서로를 계속 참조하게 되면서 무한 루프에 빠져버리게 되는 것입니다.

 

따라서, 이 문제를 해결하기 위해서 많은 사람들이 @JsonIgnore, @JsonManagedReference, @JsonBackReference 등의 어노테이션을 통해 이런 참조를 무시해버리도록 설정하는 것을 봤는데요. 해당 키워드들로 구글링을 해보시면 간단한 어노테이션 추가로 위 문제를 해결할 수도 있습니다.

 

 

하지만 애초에 저 문제가 DTO의 설계로 인해 순환 참조가 발생하는 것이라고 보면, DTO를 조금 수정하면 어떨까요?

 

 

🧑‍💻 VoteOptionDto 사용하기

위에서 작성한 VoteDto를 아래와 같이 수정했습니다.

VoteDto - (2)

VoteOption이라는 엔티티 자체를 리스트로 받지 않고, VoteOptionDto라는 DTO를 만들어주는 방식인데요. Service에서 받아온 Vote가 가지고 있던 VoteOptions를 entity.getVoteOptions()를 통해 가져오고, 아래와 같은 VoteOptionDto로 만들어줍니다.

VoteOptionDto

VoteOptionDto는 VoteOption 도메인 필드에서 Vote를 제외해서 설계합니다. 어차피 여기서 리턴되는 Dto는 Vote로부터 파생된 것이기 때문에 연관관계 매핑이 되어있다는 것은 전제되어 있기 때문이죠.

 

자, 그러면 VoteOptionDto는 Vote와 직접적으로 아무런 연관관계 매핑이 없습니다. 따라서, ObjectMapper가 JSON 매핑을 시도할 때도 서로 순환참조가 발생할 일이 없어지겠죠? 

 


이렇게 이번 시간에는 @OneToMany, @ManyToOne 관계에서 발생할 수 있는 순환참조 문제에 대한 공부해봤습니다(@OneToOne에서도 생길 수 있습니다).

 

사실 @JsonIgnore, @JsonManagedReference, @JsonBackReference과 같은 어노테이션으로 이 문제를 처리할 수도 있었는데요. 하지만 이러한 방식은 크게 두 가지 문제점(?)이 있다고 생각했습니다.

 

1. Dto 기반으로 컨트롤러, 서비스 로직을 설계했는데 정작 저 부분은 잘못된 Dto로 설계해서 생긴 문제라고 볼 수도 있다.

2. @JsonIgnore와 같은 어노테이션을 이용하면 엔티티의 필드에 손을 대는 것이기 때문에 다양한 API에 대응할 수 있는 유연성이 떨어진다.

 

따라서, 적절한 DTO 설계를 통해서 이러한 문제를 잘 해결해주는 것이 중요한 것 같습니다.

 

감사합니다.