인프런 커뮤니티 질문&답변
@OneToMany 지연 로딩 관련하여 질문 드립니다.
작성
·
323
·
수정됨
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개 발생하고 있습니다.
또한 지연로딩 클래스를 조회할때도 프록시값이 아닌 엔티티가 조회될 경우 지연로딩이 아니지 않나요?
잘못 이해한 부분이 있는지 확인 부탁드립니다!