on
[JPA] Lazy Loading이 포함된 Response 유의
[JPA] Lazy Loading이 포함된 Response 유의
이번 포스팅에서는 Lazy Loading이 포함된 엔티티를 ResponseEntity 응답 정보로 사용할때 유의 해야할 점을 다루어보려 합니다.
이전 포스팅과 동일한 프로젝트 진행중에 아래와 같은 오류를 만났습니다.
이전 포스팅을 참고해주세요.
No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ... $HibernateProxy$ hibernateLazyInitializer
오류가 발생하는 이벤트는, 사용자가 특정 지하철 노선 조회를 요청하는 경우였습니다.
1)사용자가 url path 정보에 특정 지하철 id를 포함하여 요청하면
2)특정 지하철 id로 데이터를 조회하여 LineReponse 객체를 생성한 후
3)ResponseEntity body에 담아 응답하게 됩니다.
[LineController] : 특정 지하철 노선 요청 처리 부분
@GetMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity showLine(@PathVariable Long id) { LineResponse lineResponse = lineService.findLineById(id); return ResponseEntity.ok().body(lineResponse); }
[LineService] : 특정 지하철 노선 ID로 DB에서 조회하여 결과 가져옴
public LineResponse findLineById(Long id) { Optional line = lineRepository.findById(id); return LineResponse.from(line.orElseThrow(() -> new IllegalArgumentException("없는 노선입니다."))); }
[LineResponse] : Line Entity를 응답할 dto 타입으로 변환
public static LineResponse from(Line line) { return new LineResponse(line.getId(), line.getName(), line.getColor(), line.getSections(), line.getCreatedDate(), line.getModifiedDate()); }
위의 오류에서 $HibernateProxy$ hibernateLazyInitializer SerializationFeature.FAIL_ON_EMPTY_BEANS 키워드가 눈에 띄었습니다.
[Line]
@Entity public class Line extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; ... @Embedded private Sections sections = new Sections(); ...
[Sections]
@Embeddable public class Sections { @OneToMany(mappedBy = "line", cascade = CascadeType.ALL, orphanRemoval = true) private List sections = new ArrayList<>(); ...
Line Enity는 @OneToMany 관계로 List을 참조하고 있습니다.
@OneToMany 연관관계에서는 기본적으로 Lazy Loading이 적용되기 때문에 lineRepository.findById(id) 로 조회할 경우, Line 테이블만 참조하여 데이터를 가져오고 Line에서 참조하는 Section은 프록시 객체로 생성합니다.
이후, Line Entity를 통해 getSections와 같이 직접 참조를 할 때에 실제로 Section 테이블을 조회하여 데이터를 채워넣게 됩니다.
Hibernate가 Lazy Loading에서 사용하는 프록시는 다음과 같은 구조를 가집니다.
1)프록시는 기존 Entity를 상속함으로써 Entity를 대신할 수 있습니다.
2)프록시는 직접 참조 요청이 들어왔을때 실제 Entity를 생성하고, 이렇게 생성된 Entity를 참조하게 됩니다.
저는 LineResponse.from 메서드에서 line 엔티티를 변환할때 List에 대한 참조를 하게 되니 DB에서 데이터를 가져와 실제 Entity가 생성되는데 왜 오류가 날까 하였는데요,(실제로 로그를 찍어봐도 List의 데이터는 잘 채워진 것이 확인됩니다.)
위 그림에서 키 포인트는, 실제 entity를 생성하였다고 하여서 처음 생성한 Proxy 객체를 대신해 entity를 사용하는 것이 아니고 Proxy를 통해서 entity를 참조하는 것이었습니다.
ResponseEntity의 body에 넣은 데이터를 serialize할때 class 타입이 proxy인 것이 문제가 되는 것이었습니다.
이 문제를 해결하기 위해 지연로딩을 사용하지 않으면 Line 하나를 조회할 때 마다 해당 Line에 속한 Section들을 모두 조회하는 비용이 발생하기 때문에 실무에 적용해서는 안 될 옵션이었습니다.
그래서 기존 LazyLoading 옵션을 변경하진 않고, 조회시 fecth join으로 한번에 모두 가져오도록 변경하였습니다.
이를 통해 Proxy가 아닌 실제 entity 타입을 사용하도록 함으로써 문제를 해결하였습니다.
[LineRepository]
@Query("select l from Line l " + "left join fetch l.sections.sections s " + "left join fetch s.upStation " + "left join fetch s.downStation " + "where l.id=:id") Line findLineWithSectionsAndStationsById(@Param("id") Long id);
[LineService]
public LineResponse findLineWithSectionsById(Long id) { Line line = lineRepository.findLineWithSectionsAndStationsById(id); validateExistLine(line); return LineResponse.from(line); }
[참고]
프록시 구조는 김영한님의 자바 ORM 표준 JPA 프로그래밍 책을 참조하였습니다.
from http://jerry92k.tistory.com/45 by ccl(A) rewrite - 2021-11-20 15:02:07