[bug] Entity 순환 참조로 인한 ResponseEntity 생성 무한루프
@PutMapping("/players/{id}/register/{teamId}")
public ResponseEntity<?> registerPlayer(@PathVariable Long id, @PathVariable Long teamId) {
Player player = playerService.registerTeam(teamId, id);
EntityModel<Player> entityModel = playerAssembler.toModel(player);
return ResponseEntity
.created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri())
.body(entityModel);
}
- 위 코드는 Player엔티티의 팀을 등록하는 컨트롤러 메서드입니다. PlayerService.registerTeam 메서드를 통해 DB에 Player의 팀을 수정하고, RespnseEntity를 생성하여 리턴합니다.
- cURL을 통해 Player와 Team 엔티티를 생성하고 위 메서드를 통해 Player를 등록하려 하면 아래와 같은 이상한 더미코드가 발생하며 에러를 던집니다.
$ curl -X PUT localhost:8080/players/1/register/3
{"id":1,"name":"kiin","team":{"id":3,"name":"Kwangdong Freecs","players":[{"id":1,"name":"kiin","team":{"id":3,"name":"Kwangdong Freecs","players":[{"id":1,"name":"kiin","team":{"id":3,"name":"Kwangdong Freecs","players":[{"id":1,"name":"kiin","team":{"id":3,"name":"Kwangdong Freecs","players":[{"id":1,"name":"kiin","team":{"id":3,"name":"Kwangdong Freecs","players":[{"id":1,"name":"kiin","team":{"id":3,"name":"Kwangdong Freecs","playe:"kiin","team":{"id":3,"name":"Kwangdong Freecs","players":[{"id":1,"name":"kiin"...
- 한줄 한줄 디버깅을 하여 본 결과, ResponseEntity를 생성하는 과정에서 무한루프가 발생한것 같습니다.
ReponseEntity를 생성하는 방법
EntityModel을 받아 ReponseEntity를 생성하기 위해서는 EntityModel 가지고 있는 obejct를 serialize 해야 합니다. 이때 Jackson 라이브러리가 사용됩니다. Jackson은 getter를 통해 object 필드를 인식하고 주입합니다.
엔티티의 순환참조
아래 코드는 Player 엔티티와 Team 엔티티의 일부분 입니다. @OnetoMany, @ManyToOne 애노테이션을 통해 두 엔티티가 순환참조하고 있습니다.
// player
@Entity
@Getter
public class Player {
...
@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
// Team
@Entity
@Getter
public class Team {
...
@OneToMany(mappedBy = "team", fetch = LAZY)
private List<Player> players = new ArrayList<>();
...
}
Jackson이 이제 Player 엔티티의 Getter를 사용해 Player 오브젝트를 serialize합니다. 무슨일이 일어날까요? Player는 getTeam을 호출합니다. 그러면 Team은 다시 getPlayers를 호출합니다. 그리고 이것이 반복됩니다! 즉, 무한루프에 빠지게 됩니다….
Solution
JPA는 다양한 애노테이션과 특성들을 통해 순환 참조를 쉽고 직관적으로 관리할 수 있도록 도와줍니다. 하지만 이는 JPA가 관리하는 영역에서만 한정됩니다. JPA 관리의 범위를 벗어나는 순간 순환 참조는 많은 문제들을 일으킵니다. 그러므로 client와 통신하는 과정에서는 엔티티를 그자체로 이용하기보다는 DTO를 만들어 사용하는 것이 합리적입니다.
@PutMapping("/players/{id}/register/{teamId}")
public ResponseEntity<?> registerPlayer(@PathVariable Long id, @PathVariable Long teamId) {
Player player = playerService.registerTeam(teamId, id);
EntityModel<PlayerDto> entityModel = playerAssembler.toModel(player);
return ResponseEntity
.created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri())
.body(entityModel);
}
수정된 코드에서는 assembler.toModel 메서드를 호출하면 DTO의 엔티티모델을 리턴합니다. 이러한 dto은 순환 참조에서 자유로우며 당연히 잭슨이 오브젝트를 serialize하는 과정에서 무한 루프도 발생하지 않습니다.