본문 바로가기
Spring/Spring

[JPA] JSON 직렬화 순환 참조 문제

by 작은돼지 2023. 6. 27.

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"
    }
]