[JPA] API ๊ฐœ๋ฐœ - ์ง€์—ฐ๋กœ๋”ฉ๊ณผ ์„ฑ๋Šฅ ์ตœ์ ํ™” (feat.Fetch ์กฐ์ธ)

2022. 6. 16. 10:17ใ†Backend/๐ŸŒฟ Spring

JPA ์˜ ์ง€์—ฐ๋กœ๋”ฉ ์˜ต์…˜์„ ์‚ฌ์šฉํ•  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์„ฑ๋Šฅ๋ฌธ์ œ๋ฅผ ๋‹จ๊ณ„์ ์œผ๋กœ ํ•ด๊ฒฐํ•ด๋ณด์ž. 

 

Version 1. ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ง์ ‘ ๋…ธ์ถœ (์ง€์–‘ ํ•ด์•ผํ•˜๋Š” ๋ฒ„์ „)

์•„๋ž˜ ์ฝ”๋“œ๋Š” Order ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ปฌ๋ ‰์…˜์— ๋‹ด์•„ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœํ•œ๋‹ค. ์ด๋Ÿด ๊ฒฝ์šฐ API ์ŠคํŽ™์ด ๋ณ€๊ฒฝ๋˜๋ฉด ํด๋ผ์ด์–ธํŠธ ๋‹จ์—์„œ๋Š” ๋ณ€๊ฒฝ๋œ ์ŠคํŽ™์— ๋Œ€์‘ํ•˜์ง€ ๋ชปํ•˜๊ณ  ์ž˜๋ชป๋œ ๊ฐ’์„ ์ฝ์–ด๋“ค์—ฌ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์กด์žฌํ•œ๋‹ค. ๋˜ํ•œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์•ˆ์ƒ์˜ ๋ฌธ์ œ๋„ ๊ฐ„๊ณผํ•œ๋‹ค. 

/**
 *
 * xToOne(ManyToOne, OneToOne) ๊ด€๊ณ„ ์ตœ์ ํ™”
 * Order
 * Order -> Member
 * Order -> Delivery
 *
 */
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;
    private final OrderSimpleQueryRepository orderSimpleQueryRepository; //์˜์กด๊ด€๊ณ„ ์ฃผ์ž…

    /**
     * V1. ์—”ํ‹ฐํ‹ฐ ์ง์ ‘ ๋…ธ์ถœ
     * - Hibernate5Module ๋ชจ๋“ˆ ๋“ฑ๋ก, LAZY=null ์ฒ˜๋ฆฌ
     * - ์–‘๋ฐฉํ–ฅ ๊ด€๊ณ„ ๋ฌธ์ œ ๋ฐœ์ƒ -> @JsonIgnore
     */
    @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;
    }
}

Order ์—”ํ‹ฐํ‹ฐ๋Š” Member ์™€ Delivery ๋ผ๋Š” ์—”ํ‹ฐํ‹ฐ์™€ xToOne ๊ด€๊ณ„๋ฅผ ๋งบ๊ณ  ์žˆ๋‹ค. ๋ฌด์Šจ ๋œป์ด๋ƒํ•˜๋ฉด Order์€ 1์ด๊ฑฐ๋‚˜ N์ด ๋  ์ˆ˜ ์žˆ์ง€๋งŒ, Member ์™€ Delivery๋Š” ํ•ญ์ƒ 1์ธ ๊ฒฝ์šฐ๋ฅผ ์˜๋ฏธํ•œ๋‹ค. ๊ฐ๊ฐ์˜ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์‚ดํŽด๋ณด์ž. ์„œ๋กœ๊ฐ€ ์„œ๋กœ๋ฅผ ๋‹จ๋ฐฉํ–ฅ์œผ๋กœ ์ฐธ์กฐํ•˜๊ธฐ ๋•Œ๋ฌธ์— Order-Member, Order-Delivery ๋Š” ์–‘๋ฐฉํ–ฅ ์—ฐ๊ด€๊ด€๊ณ„๊ฐ€ ํ˜•์„ฑ๋œ๋‹ค.

Order

Member ์™€ Delivery ๋ฅผ ์ฐธ์กฐํ•˜๋ฉฐ ์˜ต์…˜์€ LAZY ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค. ์ง€์—ฐ๋กœ๋”ฉ์„ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ์˜๋ฏธ์ธ๋ฐ, ์ด๊ฒƒ์„ EAGER ๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด Order ๋ฅผ ์กฐํšŒํ•˜๋Š” ์ฆ‰์‹œ Member ์™€ Delivery ๋„ ์กฐํšŒํ•œ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ํŽธํ•˜๊ณ  ์ข‹์€๊ฒƒ ์•„๋‹ˆ๋ƒ๊ณ ? ๊ทธ๋ ‡๊ฒŒ ์ƒ๊ฐํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ EAGER ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด 1) N + 1 ๋ฌธ์ œ์™€ 2) ์„ฑ๋Šฅ ์ตœ์ ํ™” 2๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ์ฆ‰์‹œ ๋กœ๋”ฉ์„ ์‚ฌ์šฉ์‹œ, Order ๋งŒ ์กฐํšŒํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ์—๋„ Member, Delivery ๊ฐ€ ๊ณง๋ฐ”๋ก ๋”ฐ๋ผ์„œ ์กฐํšŒ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ๋‹ค.  ๋•Œ๋ฌธ์— LAZY ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋”๋ผ๋„, ์œ„ ์ปจํŠธ๋กค๋Ÿฌ์˜ for๋ฌธ์œผ๋กœ  member ์™€ delivery ์— ๋Œ€ํ•ด ๊ฐ•์ œ ์ดˆ๊ธฐํ™”๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ง€ํ–ฅํ•ด์•ผํ•œ๋‹ค. ๊ทธ๋Ÿผ ๊ฐ•์ œ์ดˆ๊ธฐํ™”๋Š” ์™œ ํ•ด์•ผํ•˜๋Š” ๊ฑธ๊นŒ? Order -> Member ์™€ Order -> Delivery ๋Š” ์ง€์—ฐ๋กœ๋”ฉ์ด๋‹ค. ์ฆ‰ ์‹ค์ œ ์—”ํ‹ฐํ‹ฐ ๋Œ€์‹  ํ”„๋ก์‹œ ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์ด๊ณ , ์ด๋ฅผ ๊ทธ๋Œ€๋กœ ์›น์— ๋…ธ์ถœํ•  ์‹œ, ํ”„๋ก์‹œ ๊ฐ์ฒด๋ฅผ json ํ˜•ํƒœ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ชจ๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ์ผ์ผ์ด ๊ฐ•์ œ์ดˆ๊ธฐํ™”๋ฅผ ํ•ด์ค˜์•ผ ํ•œ๋‹ค. 

@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

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

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;


}

Member

@Entity
@Getter @Setter
public class Member {

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

    private String name;

    @Embedded
    private Address address;

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

}

Delivery

@Entity
@Getter @Setter
public class Delivery {

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

    @JsonIgnore
    @OneToOne(mappedBy = "delivery", fetch = LAZY)
    private Order order;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private DeliveryStatus status; //READY, COMP
}

* V1 ์ฒ˜๋Ÿผ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ง์ ‘ ๋…ธ์ถœํ•  ๋•Œ๋Š” ์–‘๋ฐฉํ–ฅ๊ด€๊ณ„๊ฐ€ ๊ฑธ๋ฆฐ ๊ณณ์€ ๊ผญ ํ•œ๊ณณ์„ @JsonIgnore ์ฒ˜๋ฆฌํ•ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์–‘์ชฝ์„ ์„œ๋กœ ํ˜ธ์ถœํ•˜๋ฉด์„œ ๋ฌดํ•œ ๋ฃจํ”„๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. 

 

Version 2. ์—”ํ‹ฐํ‹ฐ๋ฅผ DTO๋กœ ๋ณ€ํ™˜

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;
    private final OrderSimpleQueryRepository orderSimpleQueryRepository; //์˜์กด๊ด€๊ณ„ ์ฃผ์ž…

    /**
     * V2. ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•ด์„œ DTO๋กœ ๋ณ€ํ™˜(fetch join ์‚ฌ์šฉX)
     * - ๋‹จ์ : ์ง€์—ฐ๋กœ๋”ฉ์œผ๋กœ ์ฟผ๋ฆฌ N๋ฒˆ ํ˜ธ์ถœ
     */
    @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();
        }
    }

}

์—”ํ‹ฐํ‹ฐ๋ฅผ DTO ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ผ๋ฐ˜์ ์ธ ๋ฐฉ๋ฒ•์œผ๋กœ, V1 ๊ณผ ๋‹ฌ๋ฆฌ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ง์ ‘์ ์œผ๋กœ ๋…ธ์ถœํ•˜์ง€ ์•Š๋Š”๋‹ค.

๊ทธ๋Ÿผ ๋‹ค ํ•ด๊ฒฐ๋œ๊ฒƒ์ธ๊ฐ€? ์•„์ง ์ตœ์ ํ™”์˜ ๋ฌธ์ œ๊ฐ€ ๋‚จ์•˜๋‹ค.

์œ„ ์ฝ”๋“œ์—์„œ๋Š” ์ฟผ๋ฆฌ๊ฐ€ ์ด 1 + N + N๋ฒˆ ์‹คํ–‰๋œ๋‹ค. 

์šฐ์„  order ๋ฅผ 1๋ฒˆ ์กฐํšŒํ•œ๋‹ค. ์ด๋•Œ order ์˜ ์กฐํšŒ ๊ฒฐ๊ณผ๊ฐ€ N๊ฐœ ๋ผ๊ณ  ํ•˜์ž. 

๊ทธ๋Ÿผ order ์—์„œ member ๋ฅผ ์กฐํšŒํ•  ๋•Œ ์ง€์—ฐ๋กœ๋”ฉ์ด N ๋ฒˆ ๋ฐœ์ƒํ•˜๊ณ , ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ order ์—์„œ delivery๋ฅผ ์กฐํšŒํ•  ๋•Œ ๋˜ ์ง€์—ฐ๋กœ๋”ฉ์ด N๋ฒˆ ๋ฐœ์ƒํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด N ์ด 10์ด๋ผ๋ฉด ์ด ๋ฐœ์ƒํ•˜๋Š” ์ฟผ๋ฆฌ ๊ฐฏ์ˆ˜๊ฐ€ 1 (order) + 10 (member) + 10 (delivery) ๋กœ ์ด 21๋ฒˆ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์ด๋‹ค. ์ด๋Ÿฐ N + 1 ๋ฌธ์ œ๋กœ ์ธํ•ด ์„ฑ๋Šฅ ์ €ํ•˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ์„ ์ธ์ง€ํ•˜๊ณ  V3์—์„œ ํŽ˜์น˜ ์กฐ์ธ ์ตœ์ ํ™”๋กœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด๋ณด์ž. 

 

 

Version 3. ์—”ํ‹ฐํ‹ฐ๋ฅผ DTO๋กœ ๋ณ€ํ™˜ + ํŽ˜์น˜ ์กฐ์ธ ์ตœ์ ํ™”

 findAllWithMemberDelivery() ๋ผ๋Š” ๋ฉ”์„œ๋“œ๋งŒ ์กฐํšŒํ•˜๋ฉด V2 ์ฝ”๋“œ์™€ ๋ณ„๋ฐ˜ ๋‹ค๋ฅผ๊ฒŒ ์—†์–ด ๋ณด์ธ๋‹ค.  findAllWithMemberDelivery() ๋‚ด๋ถ€๋ฅผ ์‚ดํŽด๋ณด์ž.

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;
    private final OrderSimpleQueryRepository orderSimpleQueryRepository; //์˜์กด๊ด€๊ณ„ ์ฃผ์ž…

    /**
     * V3. ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•ด์„œ DTO๋กœ ๋ณ€ํ™˜(fetch join ์‚ฌ์šฉO)
     * - fetch join์œผ๋กœ ์ฟผ๋ฆฌ 1๋ฒˆ ํ˜ธ์ถœ
     * ์ฐธ๊ณ : fetch join์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ JPA ๊ธฐ๋ณธํŽธ ์ฐธ๊ณ (์ •๋ง ์ค‘์š”ํ•จ)
     */
    @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;
    }

    @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();
        }
    }

}

 

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();
}

JPQL ๋ฌธ๋ฒ•์—์„œ join fetch ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์ด๋ฅผ ํŽ˜์น˜ ์กฐ์ธ์ด๋ผ ํ•˜๋Š”๋ฐ, SQL ๋ฌธ๋ฒ•์ด ์•„๋‹Œ JPQL ์—์„œ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. ํŽ˜์น˜ ์กฐ์ธ์„ ์‚ฌ์šฉํ•˜๋ฉด ์—ฐ๊ด€๋œ ์—”ํ‹ฐํ‹ฐ๋‚˜ ํด๋ž˜์Šค๋ฅผ ํ•œ๋ฒˆ์— ๊ฐ™์ด ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค. ์ฆ‰ Order ๋ฅผ ์กฐํšŒํ•  ๋•Œ ์—ฐ๊ด€๊ด€๊ณ„์— ์žˆ๋Š” member ์™€ delivery ๋„ ํ•จ๊ป˜ ์กฐํšŒํ•˜๋Š” ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

์ด๋ ‡๊ฒŒ ํŽ˜์น˜ ์กฐ์ธ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ญ๊ฐ€ ์ข‹์„๊นŒ? ๋ฐ”๋กœ N + 1 ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋œ๋‹ค. ์ฆ‰ ์ฟผ๋ฆฌ 1๋ฒˆ์— Order ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ Member ์™€ Delivery ๊นŒ์ง€ ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง„๋‹ค. ํŽ˜์น˜ ์กฐ์ธ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ง€์—ฐ ๋กœ๋”ฉ ์ž์ฒด๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š์Œ์„ ๊ธฐ์–ตํ•˜์ž. 

 

 

 

* ์ถœ์ฒ˜ 
์‹ค์ „!์Šคํ”„๋ง๋ถ€ํŠธ์™€ JPA ํ™œ์šฉ 2 ๋ฅผ ์ˆ˜๊ฐ•ํ•˜๊ณ  ์ •๋ฆฌํ•œ ํฌ์ŠคํŒ…์ž„์„ ๋ฐํž™๋‹ˆ๋‹ค.