[Error] Cannot call sendError() after the response has been committed

Astin·2024년 1월 30일
0

📢 오류 발생

user 정보를 front로 보내는 과정에서 위와 같은 오류 발생

🔔 발생 원인

프론트로 데이터를 보내기 위해 json으로의 변환이 필요한데
변환과정 중에 순환참조가 발생해서 오류가 발생한 것

💯 순환참조란 ?

  • JPA에서 양방향으로 연결된 엔티티를 JSON 형태로 직렬화하는 과정에서, 서로의 정보를 계속 순환하며 참조하여 StackOverflowError를 발생시키는 현상
  1. A Entity(이하 A)와 B Entity(이하 B)가 양방향으로 연결된 상태
  2. A를 JSON으로 직렬화하기 위해 A가 참조하고 있는 B를 조회
  3. B를 조회하는 과정에서 B가 참조하고 있는 A를 조회
  4. A를 조회하는 과정에서 A가 참조하고 있는 B를 조회
  5. 무한반복... ☠️

💯 직렬화란?

객체/데이터를 바이트 형태로 변환하여 네트워크를 통해 송수할 수 있도록 만드는 것

✅ 문제 해결

해당 join 테이블이 필요없을 경우에는 간단하게 @JsonIgnore을 붙여주면 된다.

하지만 해당 테이블이 필요할 경우에는 순환참조 문제를 해결해줄 필요가 있는데 크게 @JsonMangedReference, Dto, 맵핑 재설정의 방법이 있다.

1. @ JsonMangedReference

직렬화 방향을 설정해서 참조하는 방향을 하나로 정해줌으로써 순환참조 해결

  • @JsonManagedReference

연관관계 주인 반대 Entity 에 선언

정상적으로 직렬화 수행

@OneToMany(mappedBy = "product", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JsonManagedReference
private Set<ProductUploadFile> productUploadFiles = new LinkedHashSet<>();
  • @JsonBackReference

연관관계의 주인 Entity 에 선언

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_category_id", nullable = false)
@JsonBackReference
private ProductCategory productCategory;

직렬화가 되지 않도록 수행

2. DTO(추천)

반환을 entity로 하는 경우 순환참조가 일어나므로 반환을 dto로 하면 문제 가 해결된다.

💯 추가설명

  • entity의 경우 반환시에 값을 가져오기 위해 db를 조회하는 과정에서 순환참조가 일어나지만 dto로 반환 할 경우 반환당시 정보가 이미 다 있으므로 순환참조가 일어나지 않는다.

또한 기존의 entity를 반환하는 경우, Entity 클래스를 기준으로 수많은 클래스나 비즈니스 로직들이 동작하고 있으므로 Entity를 직접 다루는 것은 이와 연관된 여러 클래스들이 영향을 받을 수 있다는 문제가 있다.

dto를 사용하는 경우 이러한 문제도 예방가능하다는 장점이 있다.

UserService

    @Transactional
    public ResponseEntity<?> retrieveUserInfo(CustomUser customUser) {
    
        Users user = customUser.getUser();
        UserInfoResponseDto dto = new UserInfoResponseDto(user);

        // 인증된 사용자 정보
        if(user != null)
            return new ResponseEntity<>(dto, HttpStatus.OK);

        // 인증되지 않은 사용자 정보
        return new ResponseEntity<>("UNAUTHORIZED", HttpStatus.UNAUTHORIZED);
    }
}

UserInfoResponseDTO

public class UserInfoResponseDto {
    private Long id;
    private String userId;
    private String name;
    private String nickname;
    private String email;
    private UserRoleDto userRoles;

    public UserInfoResponseDto(Users user){
        this.id = user.getId();
        this.userId = user.getUserId();
        this.name = user.getName();
        this.email = user.getEmail();
        this.nickname = user.getNickname();
        this.userRoles = new UserRoleDto(user.getUserRoles());
    }
    @Getter
    public static class UserRoleDto {
        private final List<Role> userRole;
        public UserRoleDto(List<UserRole> userRoleLists) {
            this.userRole = userRoleLists.stream().map(role->role.getRole()).collect(Collectors.toList());
        }
    }

}

위에서 user와 userRole은 1:N 관계로 묶여 있는데 이에 따라 순환참조가 발생했다. 따라서 이 순환참조를 풀기 위해서 UserRoleDto를 따로 만들어서 그곳에서 필요한 정보를 조회해서 UserInfoResponseDto의 값을 넣어줬다.

3. 맵핑 재설정

순환참조가 양방향 맵핑에서 오는 문제인 만큼 정말로 양방향 맵핑이 필요한지 고민해서 단방향 맵핑으로 바꾼다면 순환참조 문제를 해결할 수 있다.

💡 참고

https://velog.io/@kjyeon1101/Spring-DTO-%EB%B0%98%ED%99%98%EC%9C%BC%EB%A1%9C-%EC%88%9C%ED%99%98%EC%B0%B8%EC%A1%B0-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0

https://data-make.tistory.com/727

https://k3068.tistory.com/32

https://m.blog.naver.com/writer0713/221587351970

https://ksh-coding.tistory.com/38

0개의 댓글

관련 채용 정보