► 상황
학습중인 커피주문애플리케이션을 맨 처음단계부터 지금 배운곳까지 다시 구현해봤다. 그리고 나서 포스트맨으로 회원정보에 대한 Post요청과 Get 을 진행하는데 포스트맨 responsebody 에 1번에 해당하는 회원정보가 2만줄 넘게 뜨면서 인텔리제이 콘솔창이 계속해서 아래와같은 빨간 글씨를 만들었다.
► 에러내용 : 순환참조관계인 멤버와 스탬프가 계속해서 서로를 참조해 무한 루프를 돌며 스택오버플로우를 발생시켰다.
org.springframework.http.converter.HttpMessageNotWritableException:
Could not write JSON: Infinite recursion (StackOverflowError);
nested exception is com.fasterxml.jackson.databind.JsonMappingException:
Infinite recursion (StackOverflowError)
► 시도
1. 순환참조 상태를 해결하기 위해 Stamp 클래스 로직 수정.
스탬프 클래스의 참조 로직에서 setMember() 메서드를 제거해봤다. 사실 스택오버플로우와 관련은 없어보였지만 혹시나 해서 해봤지만 스택오버플로우는 해결되지않았다.
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
public void setMember(Member member) {
this.member = member;
if(member.getStamp() != this){
member.setStamp(this);
}
2. MemberResponseDto 클래스 수정
@Builder
@Getter
public static class Response{
private long memberId;
private String email;
private String name;
private String phone;
private Member.MemberStatus memberStatus;
private Stamp stamp;
=======// 레퍼런스코드에 있었던 메소드 //======
public String getMemberStatus() {
return memberStatus.getStatus();
}
public int getStamp() {
return stamp.getStampCount();
}
}
memberStatus 필드는 MemberStatus 열거형을 가지고 있으므로 getMemberStatus() 메서드를 별도로 만들어서 memberStatus 필드의 status 값을 리턴하도록 작성한 것이고 마찬가지로 stamp 필드는 Stamp 클래스의 인스턴스를 가지고 있으므로 getStamp() 메서드를 별도로 만들어서 stamp 필드의 stampCount 값을 리턴하도록 작성한 것이다.
responseDto 를 살펴봤다. 그리고 레퍼런스 코드와 비교해봤더니 레퍼런스 코드엔 memberStatus와 stamp 에 대한 메소드가 생성돼 있었다. (아래는 내 responseDto 이다.) 며칠전 레퍼런스 코드를 참고해가며 구현연습을 하던 중 이 메소드들을 getter로 착각해서 갑자기 게터가 왜 필요한지 이해가 안가서 일단 빼두고 작성했던 기억이 났다. 지금에서 자세히 보니 getMemberStatus 메소드의 반환값은 MemberStatus에 있는 String 타입의 status 필드인 일반 메서드였다. Stamp 또한 스탬프 클래스의 int 타입의 StampCount 필드를 반환하고 있었다. 그리고 이 로직을 추가하니 스택오버플로우 없이 정상적으로 출력됐다.
확실히 해결 후 포스트맨으로 responseBody를 비교하니 이상한 부분이 있었다. 위의 memberStatus 와 stamp 가 정상적인 출력인데 스택오버플로우가 발생한채로 계속해서 응답데이터를 쏟아내는 responseBody 속 memberStatus와 stamp 는 정상출력과 달랐다.
3. 순환참조 상태를 해결하기 위해 Member 클래스 로직 수정.
//----Member 엔티티의 Stamp 엔티티 참조로직---- //
public void setStamp(Stamp stamp) {
this.stamp = stamp;
if (stamp.getMember() != this) {
stamp.setMember(this);
}
}
결국 원인이 순환참조에서 비롯됐다는 점이 찝찝해서 member 쪽의 참조로직을 지우고 2번에서 추가한 로직도 지우고 테스트 해봤다. 그랬더니 스택오버플로우 없이 정상적으로 응답을 받았다.
하지만 memberStatus와 stamp 내용을 정상적으로 출력하기 위한 아까의 메서드를 추가해서 다시 돌리자 NullPoninterException 이 발생했고 그것은 reponse클래스의 stamp의 값을 가져올 수 없어 발생한 에러였다. 나는 아직 클래스들의 관계나 동작흐름을 완벽하게 이해하고 있지 않다보니 천천히 생각해봤다.
memberId 가 1인 회원정보를 보여줘, 라는 GET 요청에 대한 응답이 reponseDto 이다. 즉, 서비스 로직에서 아이디 1번에 해당하는 회원정보를 엔티티 객체로 넘기면 Controller에서 필요한 내용을 담은 responseDto 객체로 변환해서 반환한다. 즉, Member 엔티티 쪽의 Stamp 참조 로직을 지워버리면 Member는 Stamp 를 더이상 참조할 수 없기에 응답에 필요한 stamp 정보또한 가지고 올수 없게 된다. 그래서 NullPointerException이 발생하게된 것이다.
► 결론
<Member 엔티티 클래스>
public void setStamp(Stamp stamp) {
this.stamp = stamp;
if (stamp.getMember() != this) {
stamp.setMember(this);
}
}
스택오버플로우가 발생하는 이유는 대게 메서드호출 스택의 크기를 초과해서 발생한다. 위의 상황에선 Member와 Stamp 가 순환참조 관계였다. 이 상황에서 만약 getStamp 메서드가 없다고 생각해보자. 응답으로 보낼 Member객체의 스탬프 정보를 가지고 와 response의 stamp 필드에 담아야 한다. 즉 response 객체의 stamp필드는 Member 객체의 스탬프 정보를 담고 있다. Member객체의 setStamp메소드는 Stamp 객체에 Member객체를 설정하는데, 이때 만약 Member객체가 이미 stamp 필드에 존재한다면 스탬프객체에서 Member객체를 가져와서 다시 Member객체의 setStamp 메소드를 호출하게된다. 이 과정이 무한루프되고 스택오버플로우가 발생한다.
@Builder
@Getter
public static class Response{
private long memberId;
private String email;
private String name;
private String phone;
private Member.MemberStatus memberStatus;
private Stamp stamp;
public String getMemberStatus() {
return memberStatus.getStatus();
}
public int getStamp() {
return stamp.getStampCount();
}
}
그래서 getStamp 메서드를 통해 private Stamp stamp 필드에 가져온 값에서 stampConut 만을 가져와 int 타입으로 반환해 줌으로 써 무한 루프를 타지 않을 수 있는 거다! 즉 Member 엔티티의 멤버변수인 private Stamp stamp 를 직접참조하는 대신 Response클래스에서만 사용하는 간단한 메소드를 만들어 값을 리턴함으로써 순환참조를 방지한 것이다.
► 느낀점
클래스들의 동작 흐름이나 코드들을 익히기 위해 커피주문애플리케이션 구현을 반복해보는 중이다. 아직 대부분의 코드는 레퍼런스코드를 참고해 연습하고 있지만 그래도 첫번째 구현보다는 훨씬 나아져서 그것만으로도 뿌듯하고 홀가분하던 와중 정신이 번쩍 드는 오류였다. 어노테이션으로 매핑한 후 참조관계를 표현한 메서드가 아직은 잘 와닿지 않는다. 하지만 오늘을 계기로 그래도 어렴풋이 입력된 느낌이다. 잘하는 페어분들을 보면 항상 똑같은 말을 하신다. "반복이 중요해요. 반복만이 살길이예요." 실습애플리케이션 구현 10번 채워보자!
'문제해결' 카테고리의 다른 글
인스턴스 중지 후 재실행시 502 에러해결 (0) | 2023.07.31 |
---|---|
Mac M1 EC2 ssh 연결 불가 (0) | 2023.06.04 |
JwtAuthenticationFilter 클래스 참조 오류 (0) | 2023.05.30 |
@Value 어노테이션 컴파일오류 (0) | 2023.03.23 |
IllegalArgumentException - 구글 SMTP 메일 전송 (0) | 2023.03.12 |