인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

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

깨위님의 프로필 이미지
깨위

작성한 질문수

자바 ORM 표준 JPA 프로그래밍 - 기본편

즉시 로딩과 지연 로딩

@OneToMany 지연 로딩 관련하여 질문 드립니다.

작성

·

237

·

수정됨

0



[질문 내용]

안녕하세요! @OneToMany지연로딩 관련해서 질문 드립니다.

아래는 Team, Member 엔티티로, 연관관계를 갖습니다. (1:N)

 

  • team 엔티티

// Team.java

package hellojpa;

import jakarta.persistence.*;

import java.util.ArrayList;
import java.util.List;

@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
    private List<Member> members = new ArrayList<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Member> getMembers() {
        return members;
    }

    public void setMembers(List<Member> members) {
        this.members = members;
    }
}

 

 

  • member.class , 엔티티

// Member.java

package hellojpa;

import jakarta.persistence.*;

import javax.xml.namespace.QName;
import java.util.Date;

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Team getTeam() {
        return team;
    }

    public void setTeam(Team team) {
        this.team = team;
    }
}

 
위의 team ,member에서는 @OneToMany인데, 아래 코드 작동 시, 프록시 객체들이 조회안되고 진짜 엔티티가 조회되어 지연로딩이 발생 안합니다.

 

package hellojpa;

import jakarta.persistence.*;

import java.util.List;

public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        //code
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try{

            //저장
            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            Member member1 = new Member();
            Member member2 = new Member();
            member1.setUsername("member1");
            member1.setTeam(team);
            member2.setUsername("member2");
            member2.setTeam(team);

            em.persist(member1);
            em.persist(member2);

            em.flush();
            em.clear();



            Team findTeam = em.find(Team.class, team.getId());

            List<Member> members = findTeam.getMembers(); // 이 부분에서 프록시 객체로 조회가 되지 않습니다.! 

         

            for (Member m : members) {
                System.out.println(m.getClass()); // member.class로 콘솔 출력 됩니다.. 
                System.out.println(m.getUsername());
            }

            tx.commit();
        } catch (Exception e){
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

 

 

아래는 위의 코드 실행 시 콘솔 창입니다.



Hibernate: 
    create sequence Member_SEQ start with 1 increment by 50
Hibernate: 
    create sequence Team_SEQ start with 1 increment by 50
Hibernate: 
    create table Member (
        MEMBER_ID bigint not null,
        TEAM_ID bigint,
        USERNAME varchar(255),
        primary key (MEMBER_ID)
    )
5월 15, 2024 12:26:48 오후 org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection
INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@1fbf088b] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
Hibernate: 
    create table Team (
        TEAM_ID bigint not null,
        name varchar(255),
        primary key (TEAM_ID)
    )
Hibernate: 
    alter table if exists Member 
       add constraint FKl7wsny760hjy6x19kqnduasbm 
       foreign key (TEAM_ID) 
       references Team
Hibernate: 
    select
        next value for Team_SEQ
Hibernate: 
    select
        next value for Member_SEQ
Hibernate: 
    select
        next value for Member_SEQ
Hibernate: 
    /* insert for
        hellojpa.Team */insert 
    into
        Team (name, TEAM_ID) 
    values
        (?, ?)
Hibernate: 
    /* insert for
        hellojpa.Member */insert 
    into
        Member (TEAM_ID, USERNAME, MEMBER_ID) 
    values
        (?, ?, ?)
Hibernate: 
    /* insert for
        hellojpa.Member */insert 
    into
        Member (TEAM_ID, USERNAME, MEMBER_ID) 
    values
        (?, ?, ?)
        
        
        
        
        /////////////////        /////////////////        /////////////////
Hibernate: 
    select
        t1_0.TEAM_ID,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.TEAM_ID=?
        
        
Hibernate: 
    select
        m1_0.TEAM_ID,
        m1_0.MEMBER_ID,
        m1_0.USERNAME 
    from
        Member m1_0 
    where
        m1_0.TEAM_ID=? 
        
// 실제 객체 
class hellojpa.Member
member1
class hellojpa.Member
member2


  • for-each로 member 클래스를 출력했을 때, 프록시 객체로 조회가 되지 않으며, team.getMembers()를 실행할 때 in절로 여러개의 members엔티티를 조회해 오는 것 같습니다..

 

제가 강의를 통해 이해한 바로는, @OneToMany는 기본적으로 지연로딩이 걸려 있어, 컬렉션을 조회할 때 각 엔티티들은 '프록시'로 조회되고(지연로딩) , 각 컬렉션의 객체들에 접근할 때 추가적인 (select 문) 조회 쿼리가 발생하여 N+1문제를 낳는다고 알고 있습니다..

 

-아래는 후반부의 강의 코드 
- 강의상 지연이 발생 하는 코드 => OrderItemDto에서 N+1쿼리 발생

package jpabook.jpashop.api;


import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequiredArgsConstructor
public class OrderApiController {
    private final OrderRepository orderRepository;

    @GetMapping("/api/v1/orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
            order.getMember().getName(); //Lazy 강제 초기화
            order.getDelivery().getAddress(); //Lazy 강제 초기환
            List<OrderItem> orderItems = order.getOrderItems();
            orderItems.stream().forEach(o -> o.getItem().getName()); //Lazy 강제
        }
        return all;
    }

    @GetMapping("/api/v2/orders")
    public List<OrderDto> ordersV2(){
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        List<OrderDto> collect = orders.stream()
                .map(o -> new OrderDto(o))
                .collect(Collectors.toList());
        return collect;
    }


    @Data
    static class OrderDto{
        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;
        private List<OrdereItemDto> orderItems;

        public OrderDto(Order o) {
            orderId = o.getId();
            name=o.getMember().getName();
            orderDate=o.getOrderDate();
            orderStatus=o.getStatus();
            address=o.getDelivery().getAddress();
            orderItems = o.getOrderItems().stream() 
                    .map(orderItem -> new OrdereItemDto(orderItem))
                    .collect(Collectors.toList());
        }
    }

    @Getter
    static class OrdereItemDto{
        private String itemName; //상품명
        private int orderPrice; //주문 가격
        private int count; //주문 수량
        public OrdereItemDto(OrderItem orderItem) {
                itemName=orderItem.getItem().getName(); //문제 상황, 지연로딩 발생 
                orderPrice=orderItem.getItem().getPrice();
                count = orderItem.getCount();
        }
    }


}

 



- Order, OrderItems에서도 @OneToMany인데, 지연로딩이 발생하여, orderItems 각각의 필드값을 조회시 N+1쿼리가 나가는 것이 확인되어, 차이점이 무엇인지 알고 싶습니다.

@GetMapping("/api/v1/orders")

public List<Order> ordersV1() {

List<Order> orders = orderRepository.findAllByString(new OrderSearch());


// 지연로딩 데이터 가져오기
for (Order order : orders) {

order.getMember().getName(); // 지연로딩 초기화

order.getDelivery().getAddress(); // 지연로딩 초기화


// 2. orderItem -> getClass()

for(OrderItem o : orderItems) {

System.out.println(o.getClass()) //프록시객체

}


//3. 여기서는 select 나가서 진짜 엔티티 갖고 오는거

orderItems.stream().forEach(orderItem -> orderItem.getItem().getName()); // 상품명을 가져오기 위해서 지연로딩 강제 초기화

}

return orders;

}}

 

 

추가질문..

  • @OneToMany를 걸 경우, 기본 전략이 lazyLoading으로 알고 있습니다..
    이런 상황에서 getEntityList를 할 때, 프록시 객체가 아니라, 왜 한꺼번에 엔티티를 들고오는지 궁굼합니다..!

     

 

답변 1

0

안녕하세요. 깨위님, 공식 서포터즈 코즈위버입니다.

쿼리 실행 내용을 확인해보면 아래와 같이 Team 에 대한 조회가 먼저 발생하고,

그 이후 Member에 대한 실사용이 발생(for 문안에서 member.getClassName() / member.getName()) 할 때 Member 테이블을 다시 조회하고 있는것을 알 수 있습니다. 지연로딩이 적용된 것으로 보입니다.

Hibernate: 
    select
        t1_0.TEAM_ID,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.TEAM_ID=?
        
        
Hibernate: 
    select
        m1_0.TEAM_ID,
        m1_0.MEMBER_ID,
        m1_0.USERNAME 
    from
        Member m1_0 
    where
        m1_0.TEAM_ID=? 

 

감사합니다.

 

깨위님의 프로필 이미지
깨위
질문자

답변 감사합니다!

그런데 강의에서는 조회 쿼리문이 2개 발생할 경우 지연로딩인 것으로 이해했는데, 본 질문에서는 1개 발생하고 있습니다.
또한 지연로딩 클래스를 조회할때도 프록시값이 아닌 엔티티가 조회될 경우 지연로딩이 아니지 않나요?
잘못 이해한 부분이 있는지 확인 부탁드립니다!

깨위님의 프로필 이미지
깨위

작성한 질문수

질문하기