제리의 배움 기록

[JPA-Hibernate] Lazy Loading이 포함된 Response 유의 본문

개발기록

[JPA-Hibernate] Lazy Loading이 포함된 Response 유의

제리92 2021. 11. 20. 14:41

요약

jackson 라이브러리로 하이버네이트 프록시 객체를 serialize 할 경우 오류가 발생합니다.

1) 프록시 객체를 가져오지 않도록 join fetch를 사용하거나 2) 엔티티는 DTO로 모두 변환하여 Response body로 전달하면 문제를 피할 수 있습니다.


이번 포스팅에서는 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<LineResponse> showLine(@PathVariable Long id) {
        LineResponse lineResponse = lineService.findLineById(id);
        return ResponseEntity.ok().body(lineResponse);
    }

[LineService] : 특정 지하철 노선 ID로 DB에서 조회하여 결과 가져옴

    public LineResponse findLineById(Long id) {
        Optional<Line> 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<Section> sections = new ArrayList<>();
...

Line Enity는 @OneToMany 관계로 List<Section>을 참조하고 있습니다.

@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<Section>에 대한 참조를 하게 되니 DB에서 데이터를 가져와 실제 Entity가 생성되는데 왜 오류가 날까 하였는데요,(실제로 로그를 찍어봐도 List<Section>의 데이터는 잘 채워진 것이 확인됩니다.)

위 그림에서 키 포인트는, 실제 entity를 생성하였다고 하여서 처음 생성한 Proxy 객체를 대신해 entity를 사용하는 것이 아니고 Proxy를 통해서 entity를 참조하는 것입니다.

ResponseEntity의 body에 넣은 데이터는 jackson 라이브러리가 serialize 하게 되는데요, 이때 Proxy 인스턴스 타입이 serialize 기준을 만족하지 못하는 문제로 오류가 발생하게 되는 것이었습니다.

이 문제를 해결하기 위해 즉시로딩을 사용하면 Sections 정보가 필요없이 Line을 사용할 때에도 항상 Section 리스트를 별도로 DB에서 조회하는 문제가 있을 것으로 예상되었습니다.

그래서 조회시 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);
    }

 

과정을 진행하며 fetch join으로 한번에 다 가져오는 것 외에 또다른 방법을 알게되었습니다.

Line을 LineResponse DTO로 변환할때, List<Section> 타입도 List<SectionResponse> DTO 형태로 변환하여 담아주면 serialize 할때 proxy 타입으로 인한 문제가 발생하지 않습니다.

데이터의 계층 분리 측면에서 보았을때도, List<Section>도 DTO 형태로 반환하여 LineResponse에 담아주는 것이 더 적합하다고 생각됩니다.

  

[참고]

프록시 구조는 김영한님의 자바 ORM 표준 JPA 프로그래밍 책을 참조하였습니다.

Comments