Programming/JPA

JPA - API (지연 로딩과 조회 성능 최적화) 기본

잇(IT) 2023. 7. 21. 20:49

- xToOne (ManyToOne, OneToOne) 관계 최적화

 

1. 엔티티 직접 노출 (권장하지 않음)
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
    @GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
            order.getMember().getName(); // LAZY 강제 초기화
            order.getDelivery().getAddress(); // LAZY 강제 초기화
        }
        return all;
    }

1. 엔티티 직접 노출

2. Hibernate5Module 모듈 등록시, LAZY = null 처리

3. 양방향 관계 문제 발생하기 때문에 @JsonIgnore로 처리


@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
    @GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
             return all;
    }

(Order 엔티티의 속성으로 Member, Delivery가 xToOne 관계로 연관되어 있다.)

- 처음 Order에 있는 값들을 json을 통해 데이터를 전송하려 하면 Order 엔티티에 있는 Member, Delivery 등 수 많은 속성들을 가져와야 한다.

 

@ManyToOne(fetch = FetchType.LAZY) // 연관관계 작성
    @JoinColumn(name = "member_id") // foreign키 설정
    private Member member;
    
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

(* 양방향 연관 관계일 때 서로 계속 호출하게 되면 무한 루프에 빠질 수 있기 때문에 한 쪽에 @JsonIgnore를 설정해주어야 한다.)

- Member와 Delivery를 보게되면 LAZY로 지연로딩으로 설정되어 있다.

- 지연 로딩이기 때문에 JPA에서 실제 객체를 생성하는 것이 아닌 프록시 객체를 생성해서 임시로 넣어두었다가 실제 데이터가 필요한 시점에 DB에서 데이터를 가져와서 사용한다.

- 프록시 객체를 넣어놓는데 json은 실제 객체를 통해 데이터를 뽑아야하는데 프록시 객체를 만났기 때문에 해결 할 수 없다는 오류가 발생한다.

 

@GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
            order.getMember().getName(); // LAZY 강제 초기화
            order.getDelivery().getAddress(); // LAZY 강제 초기화
        }
        return all;
    }

- 위와 같이 직접 Member와 Delivery에 있는 데이터 정보들을 호출함으로서 DB에서 실제 데이터 값을 가져와 영속성 컨텍스트에 담게되고 프록시 객체는 실제 데이터 값을 바라보게 되면서 json에 값을 정상적으로 넣을 수 있게 된다.

 

LAZY를 강제 초기화 하지 않더라도 

@Bean
Hibernate5Module hibernate5Module() {
 	return new Hibernate5Module();
}

- Hibernate5Module을 사용하면 기본적으로 초기화 된 프록시 객체만 노출, 초기화 되지 않은 프록시 객체는 노출 안하게 사용할 수 있다.


2. 엔티티를 DTO로 변환
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
	List<Order> orders = orderRepository.findAll();
 	List<SimpleOrderDto> result = orders.stream()
 		.map(o -> new SimpleOrderDto(o))
 		.collect(toList());
 return result;
}

@Data
static class SimpleOrderDto {
 	private Long orderId;
 	private String name;
 	private LocalDateTime orderDate; //주문시간
 	private OrderStatus orderStatus;
	private Address address;
    
 	public SimpleOrderDto(Order order) {
 		orderId = order.getId();
 		name = order.getMember().getName();
	 	orderDate = order.getOrderDate();
	 	orderStatus = order.getStatus();
	 	address = order.getDelivery().getAddress();
 	}
}

1. 엔티티를 조회해서 DTO로 변환(fetch join 사용X)

2. 지연로딩으로 인해 쿼리를 N번 호출한다는 단점이 있다.


엔티티를 DTO로 변환 - 페치 조인 최적화
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
 	List<Order> orders = orderRepository.findAllWithMemberDelivery();
 	List<SimpleOrderDto> result = orders.stream()
 		.map(o -> new SimpleOrderDto(o))
 		.collect(toList());
	return result;
}

 

- OrderRepository에 findAllWithMemberDelivery() 메서드 추가

public List<Order> findAllWithMemberDelivery() {
        return em.createQuery(
                "select o from Order o" +
                        " join fetch o.member m" +
                        " join fetch o.delivery d", Order.class
        ).getResultList();
    }

 

- DTO

@Data
    static class SimpleOrderDto {
        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;

        public SimpleOrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName(); // LAZY 초기화
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
        }
    }

- JPQL와 fetch join을 사용하여 한번에 member 엔티티와 delivery 엔티티까지 조회한다.

- fetch join을 사용하였기 때문에 LAZY 지연로딩이 발생하지 않는다.

- 단, fetch join만 사용할 경우 페이징이 불가능하다는 단점이 있다

   - 페이징이 불가능 한 이유는 fetch join을 할 경우 데이터가 뻥튀기 되어 추출 기준 id가 중복된 값이 여러개 발생 할 수 있기 때문이다

- 기존 엔티티에서 조회한 값을 DTO에 값을 입력하여 DTO를 반환함으로서 원하는 데이터만 반환할 수 있다.

 

- fetch join을 할 경우 위와 같이 단 한번의 쿼리로 연관 엔티티까지 전부 조회하는 것을 볼 수 있다.


- JPA에서 DTO로 바로 조회
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
 	return orderSimpleQueryRepository.findOrderDtos();
}

 

- OrderSimpleQueryRepository 조회 전용 리포지토리

@Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {

    private final EntityManager em;

    public List<OrderSimpleQueryDto> findOrderDtos() {
        return em.createQuery(
                        "select new jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" +
                                " from Order o" +
                                " join o.member m" +
                                " join o.delivery d", OrderSimpleQueryDto.class)
                .getResultList();
    }
}

- 기존의 방식들은 엔티티를 직접 json 형식으로 데이터를 전송하거나 엔티티를 조회하여 DTO에 다시 담아 데이터를 반환하는 방식으로 데이터를 가져왔다.

- JPA에서 DTO를 바로 조회하는 방식은 엔티티를 JPQL로 조회할 때 반환값을 DTO로 반환함으로서 조회 시점에서 필요한 데이터만 가져오는 방식이다.

- 불필요한 정보까지 조회할 필요가 발생하지 않는다.

728x90

'Programming > JPA' 카테고리의 다른 글

JPA - API (지연 로딩과 조회 성능 최적화) 컬렉션 조회 최적화  (0) 2023.07.22
JPA - API 개발 기본  (0) 2023.07.19
JPA - 벌크 연산  (0) 2023.07.19
JPA - 엔티티 직접 사용  (0) 2023.07.19
JPA - 페치 조인 (2)  (0) 2023.07.19