[혼자왔니] JPA - could not initialize proxy
카테고리: RUAlone
태그: Info
영속성 컨텍스트, Proxy, DTO, 트랜잭션, Lazy Loading
상황 설명
현재 상황은 이렇다.
User와 Post가 1:N 관계를 맺고 있고, Post 엔티티에서 User에 대해 Lazy 전략으로 설정해 준 상태이다.
Client에서 Post를 조회하려고 GET 요청을 보내면 Post를 찾아 DTO로 돌려주는 가장 일반적인 형태로 구현이 되어있다.
에러가 난 코드를 살펴보자…!
// 기존 코드(Controller)
@Parameter(
name = "X-AUTH-TOKEN",
description = "로그인 성공 후 AccessToken",
required = true,
schema = @Schema(type = "string"),
in = ParameterIn.HEADER)
@Operation(summary = "게시글 단건 조회", description = "게시글을 조회합니다.")
@GetMapping("/api/post/{postId}")
public SingleResult<PostResponseDto> getPost(@PathVariable Long postId) {
Post post = postService.findByPostId(postId);
// Entity -> DTO
PostResponseDto postResponseDto = PostResponseDto.converToDto(post);
return responseService.getSingleResult(postResponseDto);
}
GET 메소드를 통해 Post에 대한 정보를 요청하고 있다.
다음과 같은 에러 메시지가 뜬다.
could not initialize proxy - no Session
Post 조회 시 어떤 일이 벌어질까?
Post를 조회하면 Post와 Lazy Loading으로 연관된 User는 바로 초기화 되지 않고 필요할 때 정보가 채워지는 Proxy 객체로 채워진다.
즉, 위 그림과 같은 상태가 되는 것이다.
Controller 단에서 Post의 값을 써서 DTO를 채워야하는데 User의 값이 초기화가 되지 않으니 DTO를 만들 수 없었던 것이다!
Lazy Loading 방식이니 DTO를 만들기 위해 데이터를 사용할 때 쿼리를 날려 Proxyr객체를 채우지 않을까?
보통 Service단에서 Transaction으로 묶어 Post를 Repository에서 찾아온다.
// 기존 코드(Service)
@Transactional(readOnly = true)
public Post getPostDetail(Long postId) {
Post post = postRepository.findPostById(postId).orElseThrow(CPostNotFoundException::new);
return post;
}
문제는 다음과 같은 이유때문에 발생한다.
JPA의 영속성 컨텍스트는 보통 트랜잭션과 생명 주기를 같이 한다.
- 서비스단에서 트랜잭션이 일어난다.
- 컨트롤러 단으로 나오면서 영속성 상태가 끝나버린다..
- 따라서, 영속성 컨텍스트에서 관리하지 않기 때문에 User에 필요한 값이 있을 때 쿼리를 날려 Proxy 객체를 채우지 않는다.
해결방법
간단하다.
Entity를 DTO로 바꾸는 작업도 Service 단에서 해주면 된다.
즉, Service 단에서 Entity를 받지 말고 Dto를 받아주면 된다.
// 개선한 코드(Controller)
@Parameter(
name = "X-AUTH-TOKEN",
description = "로그인 성공 후 AccessToken",
required = true,
schema = @Schema(type = "string"),
in = ParameterIn.HEADER)
@Operation(summary = "게시글 단건 조회", description = "게시글을 조회합니다.")
@GetMapping("/api/post/{postId}")
public SingleResult<PostResponseDto> getPost(@PathVariable Long postId) {
// 바로 서비스단에서 DTO를 받도록 변경
PostResponseDto postResponseDto = postService.findByPostId(postId);
return responseService.getSingleResult(postResponseDto);
}
// 개선한 코드(Service)
@Transactional(readOnly = true)
public Post getPostDetail(Long postId) {
Post post = postRepository.findPostById(postId).orElseThrow(CPostNotFoundException::new);
// Entity -> DTO
PostResponseDto postResponseDto = PostResponseDto.convertToDto(post);
return postResponseDto;
}
댓글 남기기