이번에 개인 프로젝트를 진행 함에 있어 1:N , N:1 양방향 관계에서 발생한 순환 참조 이슈를 겪어 포스팅합니다.
순환 참조가 일어나는 이유부터 같이 알아봅시다.
Spring boot는 @ResponseBody를 선언 할시
Object를 json 상태로 변환하기 위해 HttpMessageConverters에서 jackson 라이브러리를 이용합니다.
자 여기서 spring boot가 jackson을 이용하니
jackson의 직렬화의 동작방법을 간단히 알아봅시다.
(www.baeldung.com/jackson-field-serializable-deserializable-or-not) 해석입니다.
Jackson의 작동방법은 기본적으로
멤버 변수의 접근 지정자를 우선적으로 봅니다.
public class MyDtoAccessLevel {
private String stringValue;
int intValue;
protected float floatValue;
public boolean booleanValue;
// NO setters or getters
}
위와 같은 코드처럼 클래스의 4 개 필드 중 public 인 booleanValue 만 JSON으로 직렬화됩니다.
@Test
public void givenDifferentAccessLevels_whenPublic_thenSerializable()
throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
MyDtoAccessLevel dtoObject = new MyDtoAccessLevel();
String dtoAsString = mapper.writeValueAsString(dtoObject);
assertThat(dtoAsString, not(containsString("stringValue")));
assertThat(dtoAsString, not(containsString("intValue")));
assertThat(dtoAsString, not(containsString("floatValue")));
assertThat(dtoAsString, containsString("booleanValue"));
}
여기서 비 공용인 필드를 직렬화 하기 위해선
비 공용 필드에 대한 getter 메서드가 존재해야 해당 필드에 대해 직렬화를 진행할 수 있습니다.
자 그럼 이제 순환 참조 문제를 만나봅시다.
@Entity
@EqualsAndHashCode(of = "id")
@Getter @Setter
@Builder @AllArgsConstructor @NoArgsConstructor
public class Member {
@Id
@GeneratedValue
private Long id;
.... 생략.......
@OneToMany(mappedBy = "member") // 양방향 연관관계
private Set<OrderTable> orderList;
@Entity
@EqualsAndHashCode(of = "id")
@Getter @Setter
@Builder @AllArgsConstructor @NoArgsConstructor
public class OrderTable {
@Id @GeneratedValue
private Long id;
.....생략......
@ManyToOne // 양방향 연관관계의 주인
private Member member;
Domain을 간단히 설명하자면 Member(회원) , OrderTable(주문)입니다.
회원은(1) < -> 주문(N) 관계
회원을 통해서도 주문을 확인해야 하고 / 주문에서도 회원을 확인해야 해서 양방향 맵핑을 한 상태입니다.
관계 설정이 끝났으니 이 상태로 클라이언트가 주문 객체를 보고 싶다고 요청을 보내왔을 때
서버에서는 주문 객체를 Json 형태로 직렬화 하여 body 부분에 응답을 보낼 예정입니다.
그런데 요청의 Body 부분을 살펴보면 다음과 같이 순환 참조가 발생합니다.
동작을 그림으로 설명하자면 다음과 같습니다.
1. 요청을 받아 비즈니스 로직을 처리하고
2. 응답을 Json으로 변환하는 과정에서 JackSon 이 사용됩니다.
바로 여기서 문제가 발생합니다
앞선 Jackson 작동 방법을 알았으니 하나하나 과정을 알아보면 다음과 같습니다.
1) OrderTable의 많은 멤버 변수들 중 하나인 Member의 객체를 직렬화를 진행합니다.
2) Member의 변수들 중 List <OrderTable>가 존재합니다 그럼 다시 OrderTable을 직렬화를 진행합니다.
3) OrderTable을 직렬화를 진행하다 보니 다시 Member 가 있으니 다시 직렬화를 진행합니다.
위와 같은 과정으로 계속 상호 참조가 일어나 무한루프에 빠지게 됩니다.
서론이 길었지만 Jackson 이 객체를 직렬화 하는 과정에서 생기는 문제입니다.
자 이제 문제가 왜 발생했는지도 알았으니 해결을 해봅시다.
문제 해결은 쉽습니다.
양방향 맵핑을 진행한 두 객체들 중 한 객체에 대해서 직렬화를 진행하지 않으면 오류는 해결됩니다.
그래서 이러한 문제점을 해결하기 위해서 JackSon이 제공해주는 Annotation을 사용하면 됩니다.
1) @JsonIgnore
적용 범위는 다음과 같고 하는 역할은 직렬화 하는 대상에서 제외시킵니다.
2) @JsonManagedReference, @JsonBackReference
이 애노테이션들은 양방향 연결에서 발생하는 상호 참조를 해결할 수 있게 설계되어있다.
@JsonManagedReference
-> 정상적으로 직렬화됨
@JsonBackReference
->직렬화 하지 않도록 막음(역직렬화 중에 해당 필드 값은 Managed(포워드) 링크가 있는 인스턴스로 설정됨.)
저는 해당 방법을 사용하여 오류를 해결하겠습니다.
@Entity
@EqualsAndHashCode(of = "id")
@Getter @Setter
@Builder @AllArgsConstructor @NoArgsConstructor
public class Member {
@Id
@GeneratedValue
private Long id;
.... 생략.......
@OneToMany(mappedBy = "member") // 양방향 연관관계
@JsonManagedReference // 추가
private Set<OrderTable> orderList;
@Entity
@EqualsAndHashCode(of = "id")
@Getter @Setter
@Builder @AllArgsConstructor @NoArgsConstructor
public class OrderTable {
@Id @GeneratedValue
private Long id;
.....생략......
@ManyToOne // 양방향 연관관계의 주인
@JsonBackReference //추가
private Member member;
설정하고 재요청 결과
여기까지 양방향 관계에서 순환 참조가 일어나는 경우와 해결방법을 살펴보았는데요
사실 Response의 반환 타입으로 Entity 객체 자체를 넘기는 행위를 피하는 것이 더욱 알맞은 방법이라고 생각합니다.
따라서 반환 타입의 DTO를 새로 만드는 것은 어떨까요?
본문에 오류가 존재하거나 질문이 있다면 댓글로 남겨주세요
감사합니다.
'Spring > JPA' 카테고리의 다른 글
[JPA] findById() , getById() 에 대한 생각 (1) | 2021.07.27 |
---|---|
[JPA] 비관적 락 , 낙관적 락 (4) | 2021.05.24 |