인프런 커뮤니티 질문&답변

kwj3591님의 프로필 이미지

작성한 질문수

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

간단한 주문 조회 V1: 엔티티를 직접 노출

처음에 무한 Loop 을 돌게 되는 이유

작성

·

4K

·

수정됨

2

안녕하세요~ 강의 중 처음에 order 들을 조회하는 api 가 무한 루프를 도는 이유가 궁금해서 질문을 올리게 되었습니다 (6분 30초 ~ 7분 부분)

우선, Order > Member > Order 조회 의 순으로 일어나기 때문에 무한 루프가 발생한다고 설명해주셨습니다.

이 때, Order 는 member 가 Lazy 설정이 되어 있고, Member 에도 List<Order> 를 가져오는 것이 Lazy 로 설정이 되어 있습니다 (@OneToMany 이기 때문) .

[모든 order 를 조회하라] 라는 로직에 member 를 join 해서 가져오기 대문에, Order 가 Member 를 조회해서 가져오는 부분은 이해를 하였습니다. 쿼리도 다음과 같이 나가더라구요!

2023-01-15 18:08:52.603 DEBUG 74756 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        order0_.order_id as order_id1_6_,
        order0_.delivery_id as delivery4_6_,
        order0_.member_id as member_i5_6_,
        order0_.order_date as order_da2_6_,
        order0_.status as status3_6_ 
    from
        orders order0_ 
    inner join
        member member1_ 
            on (
                order0_.member_id=member1_.member_id
            )
2023-01-15 18:08:52.713 DEBUG 74756 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        member0_.member_id as member_i1_4_0_,
        member0_.city as city2_4_0_,
        member0_.street as street3_4_0_,
        member0_.name as name4_4_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?
2023-01-15 18:08:52.722 DEBUG 74756 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        orders0_.member_id as member_i5_6_0_,
        orders0_.order_id as order_id1_6_0_,
        orders0_.order_id as order_id1_6_1_,
        orders0_.delivery_id as delivery4_6_1_,
        orders0_.member_id as member_i5_6_1_,
        orders0_.order_date as order_da2_6_1_,
        orders0_.status as status3_6_1_ 
    from
        orders orders0_ 
    where
        orders0_.member_id=?

첫번째 쿼리로 join 을 하는데, <이후 호출>에 의하여 두, 세번째 쿼리가 나가는 것 같습니다.

  1. 이 때, 각각의 멤버의 정보를 불러오기 위하여 member 쿼리가 나가는 것은 이해가 되는데, orderItem 이 LAZY 임에도 쿼리가 또 나가는 이유가 궁금합니다. response 가 나가는 과정에서 Json 형성을 위해 ObjectMapper 가 getMember, getOrderItem 등을 수행하는 것으로 알고 있는데, 이 때 불러오기 때문일까요??

 

2.

ObjectMapper 가 무시할 수 있도록 다음과 같이 변경후 실행시켜 보았습니다.

    @JsonIgnore
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

이렇게 될 시엔 다음과 같이 Serializable 관련 에러가 발생하는 것 같았습니다. 그랬더니 두번재 쿼리까지만 나가긴 하는데, 다음과 같은 에러가 발생합니다.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 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) (through reference chain: java.util.ArrayList[0]->com.example.actualjpa.domain.Order["member"]->com.example.actualjpa.domain.Member$HibernateProxy$jMkxupIu["hibernateLazyInitializer"])

검색을 해보니 프록시 객체를 Serialize 하려고 했다는 에러가 발생하는 것 같은데, orders 에 프록시 값들이 들어있는게 맞을까요? 반대인 경우에는 DB 에 FK값이 있기 때문에 id 값만을 가지고 Proxy 객체를 형성한다는 점을 완전히 이해를 했었는데, 이와 같은 경우에는 각 ID 값이 DB에 없기 때문에, Member만 조회시 orders 내의 Order 들에 대한 프록시도 없고 아예 아무 정보도 없겠구나 라고 이해를 했었습니다.
즉, 레이지일 경우 Member만 조회시 orders 에 대한 정보는 Member 객체에 없다 라고 이해를 했었습니다. @JsonIgnore를 해서 조회같은걸 할 수 없도록 했음에도, 위와 같은 에러가 발생하는 이유가 어떤건지 알 수 있을까요??

 

=========================

위 질문에 대한 수정사항:

제가 질문한 바로 뒷내용이 JsonIgnore 설정해주시고 같은 에러에 대해서 설명해주시는 부분 확인했습니다 ㅠㅠ 프록시 객체 때문에 발생하는 에러가 맞네요.

그래도 같은 질문을 드리고 싶은데, 1:N 관계에서 Proxy 객체에 대해서 조금 이해가 안되었습니다(JPA 강의 완강했습니다). Apple 과 Tree 라는 N:1 관계에서 살펴보면 (양방향 모두LAZY 설정시) , Apple Entity 내의 private Tree tree가 있을 것이고, APPLE DB에는 외래키값으로 tree_id 가 들어가있을 것이기 때문에 그 값을 토대로 프록시 객체를 형성해 놓는다는 사실은 잘 이해하였습니다.

하지만 그 반대일 경우 에 대해서 이해를 못한 것 같습니다 (JPA 강의에서도 반대에 대한 프록시 구성 설명은 없었던 것 같습니다 ㅠ). 반대의 경우 Tree DB 안에는 Apple 을 외래키값으로 가지고 있지 않은데, 어떻게 프록시 객체를 형성해 놓는 걸까요?? (프록시 객체를 형성해 놓기 때문에 위 2번과 같은 상황이 발생한 것으로 보입니다). 다음과 같은 예제를 설정해보았습니다.

@Test
@DisplayName("1:N에서 1이 N List를 조회시 :: LAZY LOADING 의 타입은 Proxy")
void test2() {

    Tree findTree = em.find(Tree.class, tree.getId());
    System.out.println("findTree.getApples().get(0).getClass() = " + findTree.getApples().get(0).getClass());

}

이 때, em.find 만 해서 tree 를 조회하였을 경우 쿼리가 Tree 만을 조회하도록 나가는 것은 확인했습니다. 하지만, 그 아래 get(0) 를 수행하는 순간 "Apple 테이블에서 Tree_ID={tree_id} 인 값을 찾아와라" 라고 쿼리 문이 나가는 것을 확인했습니다. (getClass 를 통해 프록시가 맞는지 확인하려 했으나, get() 을 하는 순간 select 가 나가서 확인하지 못했습니다)

즉,
Apple 에서 Tree 프록시를 형성할 때는 Apple DB에 있는 외래키를 통해 Tree Proxy 에 PK 값을 넣어준 상태로 프록시를 형성하였는데,
Tree 에서 Apple 들의 프록시를 형성할 때는 외래키가 없으므로, 그냥 자신의 PK 값을 통해 Apple 의 외래키 값을 넣어준 상태로 프록시를 형성해 둔다(??).
이렇게 정리되는게 맞는 걸까요?

질문이 너무 기네요.... 계속 붙이다 보니 .... 죄송합니다. 2번 질문은 그냥 아래 수정사항 이후 질문이라고 봐주시면 될 것 같습니다.

답변 2

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. kwj3591님

N:1의 경우 프록시 객체가 필요합니다.

그런데 1:N의 경우 프록시 객체라기 보다는 사실은 특별한 컬렉션을 가지게 됩니다.

이 컬렉션은 프록시 처럼 동작하는데, 데이터를 조회하는 순간 내부 데이터를 초기화 합니다.

(우리가 일반적으로 이것을 프록시로 이해해도 괜찮습니다.)

그리고 해당 데이터를 조회하기 위해서 연관 정보의 메타데이터를 확인해서 보통 자신의 PK 값을 통해 연관된 곳의 FK를 조회해서 데이터를 불러오게 됩니다.

감사합니다.

0

kwj3591님의 프로필 이미지
kwj3591
질문자

답변이 .. .. . .. ㅠ

kwj3591님의 프로필 이미지

작성한 질문수

질문하기