본문 바로가기
[서버]/[SpringBoot Kotlin]

순환참조가 문제 삼아지는 시기

by Hevton 2023. 4. 10.
반응형
@EntityGraph(attributePaths = ["comments"])
fun findOneWithCommentsById(id: Long) : Optional<Marathon>

위 코드는 동일한 전제라고 본다.

 

 

 

순환참조 발생 안함

@GetMapping("/marathon/{id}")
fun getMarathon(@PathVariable id: Long): ResponseEntity<MarathonResponse> {

return ResponseEntity.ok(MarathonResponse.of(marathonService.getMarathon(id)))
}
@Transactional
fun getMarathon(id: Long): Marathon {

val marathon = marathonRepository.findOneWithCommentsById(id).orElseThrow()

return marathon

}

 

 

 

순환참조 발생안함

@GetMapping("/marathon/{id}")
fun getMarathon(@PathVariable id: Long): ResponseEntity<MarathonResponse> {

return ResponseEntity.ok(marathonService.getMarathon(id))
}
@Transactional
fun getMarathon(id: Long): MarathonResponse {

val marathon = marathonRepository.findOneWithCommentsById(id).orElseThrow()

return MarathonResponse.of(marathon)

}

 

 

 

순환참조 발생

@GetMapping("/marathon/{id}")
fun getMarathon(@PathVariable id: Long): ResponseEntity<Marathon> {

return ResponseEntity.ok(marathonService.getMarathon(id))
}
@Transactional
fun getMarathon(id: Long): Marathon {

val marathon = marathonRepository.findOneWithCommentsById(id).orElseThrow()

return marathon

}

 

 

일단 모든 @ManyToMany, @OneToMany은 기본적으로 FetchType.LAZY이고

@ManyToOne은 기본적으로 FetchType.EAGER이다.

 

LAZY로 설정하게 되면, 실제 필드를 언급하거나 할 때에만 요청된다.

 

N + 1 문제란, 쿼리가 불필요하게 한번 더 쓰일 수 있다는 점인데, 이걸 해결하기 위해선 Fetch join 쿼리를 만들어야하고

이는 @EntityGraph로 해결할 수 있기도 하다. 

 

FetchType.EAGER를 쓰던 FetchType.LAZY를 쓰고 나중에 필드를 요청하건, 쿼리 수는 똑같다. 즉시 로딩 여부의 차이 뿐이다.

 

@EntityGraph는 조금 다르다. 한번의 쿼리로 EAGER하게 불러온다.

 

 

그리고 위 실험을 통해 얻어낸 결과는, Controller에서 return을 할 때에 toString 같은 함수를 호출해야 하기 때문에

LAZY 였던 필드들도 호출이 되고, 무한 순환참조가 발생할 여지가 있었다면 이럴 때 결정적으로 발생할 수 있다는 것이다.

 

 

결론적으로.. 모든 순환참조가 문제가 되는 부분은,

repository에서 가져온 객체를 받아올 때도 아니고 그걸 결국 json으로 출력하던가 할 때 발생한다고 볼 수 있다.

그건 repository에서 가져온 객체가 현재 테이블 구조상 무한참조상황이 100퍼센트인 상황이더라도 그렇다.

 

순환참조란 JPA에서 양방향으로 연결된 엔티티를 JSON 형태로 직렬화하는 과정에서 발생하는 것이다.

객체를 불러오는 그 시기 자체에선 문제로 삼아지진 않는다.

 

 

그래서 여기선 문제가 발생하지 않았던 것. 

val fcmList = marathonRepository.getReferenceById(request.topic.toLong()).subscribers.map {
u -> u.fcm
}.toList()

이렇게 변환하고 나중에 Controller에서 정제된 데이터를 반환하므로.

https://dev-coco.tistory.com/133

 


 

JPA Repository를 사용한 쿼리 자체는 

관계가 LAZY면 해당되는 데이터만, EAGER면 연관된 것 까지 join을 통해 가져오게 된다.

 

이 부분의 쿼리 자체에서 각 관계가 EAGER라고 한들 순환참조가 무한으로 발생하진 않는다. 

LAZY 면 join 안하고, EAGER면 join 하고. A와 B가 서로 ManyToMany인데 EAGER라고 해서

A Repository.findAll() 했을때 A -> B -> A -> B .. 하지 않는다는 것이다.

그냥 한 번만 join 하고 끝난다. 결국 이건 쿼리고, id 값 같은 foreign key로 join 될 뿐이다.

 

 

문제는 JSON 직렬화 할 때 무한으로 순환되어 발생한다는 것이다.

아래 쿼리 비교를 보면 더 와닿을 수 있다.

https://velog.io/@jin0849/JPA-즉시로딩EAGER과-지연로딩LAZY

 

[JPA] 즉시로딩(EAGER)과 지연로딩(LAZY) (왜 LAZY 로딩을 써야할까?) (1)

이전 글에서 Proxy에 대해 살펴보았다. Proxy는 이 글의 주제인 즉시로딩과 지연로딩을 구현하는데 중요한 개념인데, 일단 원리는 미뤄두고 즉시로딩과 지연로딩이 무엇인지에 대해 먼저 알아보자

velog.io

 

반응형

'[서버] > [SpringBoot Kotlin]' 카테고리의 다른 글

Docker Nginx  (0) 2023.04.11
Docker 사용하기  (0) 2023.04.11
ManyToMany 관계에 데이터 추가(INSERT) 하기  (0) 2023.04.10
유저와 게시글 N : M 에서의 Relation  (0) 2023.04.07
Ubuntu에 Docker 설치하기  (0) 2023.04.07