java.lang.StackOverflowError: null

젤리젤링텀·2024년 3월 20일
0

Spring, SpringBoot

목록 보기
8/8

에러

java.lang.StackOverflowError: null

오류 내용

모든 게시물(Post)을 불러오는 API를 제작하던 도중, API를 호출했더니 Item에서 Post를 참조하고, Post에서 User를 참조하고, User에서 Post를 참조하고, Post에서 User를 참조하고... 순환참조 발생으로 StackOverflow 에러 발생!

User 엔티티

@AllArgsConstructor //
@NoArgsConstructor //
@Entity
@Getter
@Builder
@Table(name = "user") //h2 2.x 이상버전에서는 USER 키워드 사용이 금지
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;

   	...
	
    @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
    private List<Post> posts;

Item 엔티티

@AllArgsConstructor
@NoArgsConstructor
@Entity
@Getter
@Builder
@Table(name = "item",indexes = {
        @Index(columnList = "name_kr")
})
public class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "item_id")
    private Long id;
    
    ...

    @OneToMany(mappedBy = "item")
    private List<Post> posts;

Post 엔티티

@AllArgsConstructor
@NoArgsConstructor
@Entity
@Getter
@Setter
@Builder
@Table(name = "post")
public class Post {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "post_id")
    private long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;
    
    ...

문제 원인

연관관계의 주인이 아닌 User와 Item에서는 DB에 따로 Post의 FK가 있지는 않지만, JPA는 서버에서 클라이언트로 User나 Item의 정보를 보내줄 때, Post 정보도 같이 보내주게 된다.

연관관계의 주인인 Post에서는 DB에 따로 User와 Item에 대한 FK가 있으며, JPA는 마찬가지로 서버에서 클라이언트로 Post 정보를 보내줄 때, User와 Item 정보도 같이 보내주게 된다.

JPA에서 양방향으로 연결된 Entity를 그대로 조회하는 경우 서로의 정보를 순환하면서 조회하다가 stackoverflow가 발생하게 된다.
※ Spring Boot는 @ResponseBody(rest api)를 구현할 시 Object를 JSON 형태로 변환하기 위해 Jackson 라이브러리를 이용하는데, Jackson은 entity의 getter를 호출하고, 직렬화를 이용해 JSON 형태로 객체를 변화시키고 view로 전달한다.
getter를 호출하는 과정에서부터 순환 참조가 계속 발생해 view로 전달하면서 stackoverflow가 발생하게 된다.
※ 직렬화란, 객체의 내용을 바이트 단위로 변환하여 파일 또는 네트워크를 통해 스트림(송수신)하도록 하는 것을 의미한다.

순환 참조 해결 방법

1. @JsonIgnore 

이 어노테이션을 붙이면 JSON 데이터에 해당 프로퍼티는 null로 들어가게 된다.
즉, Json 데이터에 아예 포함시키지 않는다.

2. @JsonManagedReference 와 @JsonBackReference

부모 클래스(연관 관계의 반대편, User, Item)의 posts 필드에 @JsonManagedReference를, 자식 클래스(연관 관계의 주인, Post)의 user, item 필드에 @JsonBackReference를 추가해주면 순환 참조를 막을 수 있다.
-> Json 데이터에 포함되지 않는다. (강제로 직렬화를 막는 것)

3. @JsonIgnoreProperties

부모 클래스(User, Item)의 posts 필드에 @JsonIgnoreProperties({"user"}) or @JsonIgnoreProperties({"item"}) 를 붙여주면 순환 참조를 막을 수 있다.

4. DTO 사용 (추천!)

위와 같은 상황이 발생하게된 주원인은 '양방향 매핑'이기도 하지만, 더 정확하게는 Entity 자체를 response로 리턴한데에 있다. entity 자체를 return 하지 말고, DTO 객체를 만들어 필요한 데이터만 옮겨담아 Client로 리턴하면 순환 참조 관련 문제는 애초에 방지 할 수 있다.

5. 매핑 재설정

양방향 매핑이 꼭 필요한지 다시 한번 생각해볼 필요가 있다. 만약 양쪽에서 접근할 필요가 없다면 단방향 매핑을 해줘서 자연스레 순환 참조 문제를 해결하자.

출처: https://dev-coco.tistory.com/133 [슬기로운 개발생활:티스토리]

나의 해결 방법

PostDto.java

public class PostDto {
	@Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class PostListResponseDto {
        private User user;
        private Item item;
        private Long price;
        ...

		//Entity -> DTO
        public PostListResponseDto(Post post){
            this.user = post.getUser()
            this.item = post.getItem();
            this.price = post.getPrice();
            ...
        }
    }
}

하지만 나는 이미 DTO를 사용해서 개발했는데 왜 순환참조 오류가 나는거지..? 하고 서칭을 계속 해본 결과

PostDto 를 JSON으로 직렬화하려면, PostDTO의 필드인 UserItem도 JSON으로 직렬화해서 보여줘야한다.
그런데, UserItem을 보면 둘 다 Post를 참조하고 있다.
그럼 Post를 또 보면 UserItem을 참조하고 있고, UserItem는 또 Post를 참조하고 있고...

따라서 DTO안에 DTO를 해야하나 싶었는데.. 더 간편한 방법을 찾게 되었다.

PostDto.java 수정 후

public class PostDto {
	@Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class PostListResponseDto {
        private Long userId;
        private Long itemId;
        private Long price;
        ...

		//Entity -> DTO
        public PostListResponseDto(Post post){
            this.userId = post.getUser().getId();
            this.itemId = post.getItem().getId();
            this.price = post.getPrice();
            ...
        }
    }
}

출처: https://velog.io/@kjyeon1101/Spring-DTO-반환으로-순환참조-오류-해결하기

User와 Item의 ID 값만 가져오도록 수정하였다!

굳이 ID가 아니더라도, User의 name이 필요하다면,

public class PostDto {
	@Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class PostListResponseDto {
        private String username;
        private Long itemId;
        private Long price;
        ...

		//Entity -> DTO
        public PostListResponseDto(Post post){
            this.username = post.getUser().getUsername();
            this.itemId = post.getItem().getId();
            this.price = post.getPrice();
            ...
        }
    }
}

위와 같이 변형해서 사용하면 될 것이다.

인프런 질문 글에서도, DTO안에 엔티티를 넣는 것은 권장하지 않는다고 한다.
https://www.inflearn.com/questions/883901/comment/262481

인프런 김영한 실전 JPA 활용 2편의 지연 로딩과 조회 성능 최적화 파트를 참고하자.

결과

드디어 순환 참조 문제 해결!

profile
열심히 살자

0개의 댓글