Entity A와 Entity B가 서로 양방향 관계일 때
JPA 같은 ORM 기술 환경에서 JSON 형태로 직렬화하는 과정에서
서로(field)를 계속 순환 참조하여 무한 루프에 빠지는 현상이 발생한다.
JSON 직렬화: Java Object를 JSON 형식으로 변환하는 것
순환 참조 유도
@Getter
@Setter
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
@Getter
@Setter
@Entity
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
엔티티를 직접 사용하는 경우 순환 참조 문제가 발생 확인을 위해서
두 엔티티가 서로를 참조하도록 간단한 클래스를 작성한다.
member에 team을 집어 넣어주기 위해 @Setter를 추가한다.
확인용 컨트롤러 작성
@RequiredArgsConstructor
@RestController
public class SampleController {
private final MemberRepository memberRepository;
private final TeamRepository teamRepository;
@GetMapping("/find-members")
public List<Member> findAllMember() {
return memberRepository.findAll();
}
@GetMapping("/find-teams")
public List<Team> findAllteam() {
return teamRepository.findAll();
}
@GetMapping("/add-member")
public void createMember(@RequestBody Member member) {
Team team = new Team();
team.setName("팀 이름 1");
teamRepository.save(team);
member.setTeam(team);
memberRepository.save(member);
}
}
데이터가 없으면 순환 참조가 당연히 발생하지 않는다.
데이터를 강제로 넣어주기 위해 간단한 컨트롤러를 작성한다.
team 객체를 생성 후 member에 넣는다.
조회
의도했던 순환 참조 문제가 발생했다.
Jackson 라이브러리가 field를 직렬화하는 과정에서
순환 참조 문제가 발생하여 스택 트레이스가 무한히 반복된 모습이다.
순환 참조 막기
1. @JsonIgnore 사용
@Getter
@Setter
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "team_id")
@JsonIgnore
private Team team;
}
순환 참조를 막는 방법은 적어도 둘 중 하나의 필드를 직렬화에서 제외시켜야 한다.
@JsonIgnore는 특정 필드를 직렬화에서 제외(null)시키는 어노테이션이다.
여기서는 team 필드에 @JsonIgnore를 걸었다.
Hibernate:
select
member0_.id as id1_0_,
member0_.team_id as team_id2_0_
from
member member0_
Hibernate:
select
team0_.id as id1_1_0_
from
team team0_
where
team0_.id=?
순환 참조 문제는 해결되었다.
SELECT문이 2번 나가긴하는데 순환 참조에 관한 글이기 때문에 Lazy 로딩은 제외했다.
가장 간단하게 해결할 수 있는 방법이지만
해당 필드를 무시하기 때문에 응답 결과를 생성할 때 문제가 발생할 수 있다는 단점이 있다.
응답 시 무시한 필드의 데이터가 필요한 경우 다른 방법을 사용해야 한다.
[
{
"id": 1,
"name": "홍길동"
}
]
응답 결과를 보면 @JsonIgnore에 걸려서 무시된 필드 team 데이터가 없다.
[
{
"id": 1,
"name": "팀 이름 1",
"members": [
{
"id": 1,
"name": "홍길동"
}
]
}
]
다만 Team의 입장에서 조회하면 member는 @JsonIgnore를 걸지 않았기 때문에 조회가 된다.
2. @JsonManagedReference, @JsonBackReference 사용
@ManyToOne
@JoinColumn(name = "team_id")
@JsonBackReference
private Team team;
@OneToMany(mappedBy = "team")
@JsonManagedReference
private List<Member> members = new ArrayList<>();
@JsonManagedReference은 연관 관계의 주인 쪽에 선언하고
@JsonBackReference은 주인이 아닌 쪽에 선언한다.
@JsonBackReference가 붙은 쪽이 직렬화에서 제외된다.
[
{
"id": 1,
"name": "홍길동"
}
]
[
{
"id": 1,
"name": "팀 이름 1",
"members": [
{
"id": 1,
"name": "홍길동"
}
]
}
]
3. DTO 사용
@Getter
@Setter
@NoArgsConstructor
public class MemberDto {
private Long id;
private String name;
private String teamName;
@Builder
public MemberDto(Long id, String name, String teamName) {
this.id = id;
this.name = name;
this.teamName = teamName;
}
public static MemberDto toDto(Member member) {
return MemberDto.builder()
.id(member.getId())
.name(member.getName())
.teamName(member.getTeam().getName())
.build();
}
}
MemberDto(Response)을 별도로 만들어서
Entity가 아닌 응답으로 Dto를 내려주는 방법이다.
응답 데이터에 identifier, name, teamName이 필요하다면
필요한 데이터 필드를 정의한 DTO 클래스로 응답한다.
중요한 것은 DTO 클래스를 생성할 때
연관 관계에 있는 DTO끼리 참조하도록 작성하면 순환 참조 문제가 다시 발생할 수 있다.
만약 teamName 대신 Team team으로 하게되면 또 서로를 참조하게 되는 것이다.
[
{
"id": 1,
"name": "홍길동",
"teamName": "팀 이름 1"
}
]
'Spring > Spring' 카테고리의 다른 글
[Spring Security] X-Frame-Options와 정적 파일 로드 문제 (0) | 2023.07.18 |
---|---|
[JPA] Entity에 setter 사용을 지양하는 이유 (0) | 2023.07.15 |
[Spring Security] 스프링 시큐리티 인증 아키텍처 (0) | 2023.06.07 |
[Spring Security] 사장된 WebSecurityConfigurerAdapter (0) | 2023.06.06 |
ApplicationContext와 Spring Bean (0) | 2023.01.28 |