개발 이론/JPA

[JPA] API 개발시 주의사항 및 성능 최적화

dal_been 2023. 11. 2. 00:46
728x90

API개발시 주의사항

 

요청값으로 엔티티를 직접 받지 말자

 

문제점

- 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다

- 실무에서 회원 엔티티를 위한 다양한 API가 만들어지는데 각각의 API를 위한 모든 요청 요구사항을 담기 어렵다

- 엔티티가 변경되면 API스펙이 변한다

 

따라서 별도의 DTO로 파라미터를 받는다

 

@RestController
@RequiredArgsConstructor
public class MemberApiController {
		
		private final MemberService memberService;

		@PostMapping("/api/v2/members")
		public CreateMemberResponse saveMemeberV2(@RequestBody @Valid CreateMemberRequest request) {
				Member member = new Member();
				member.setName(request.getName);

				Long id = memberService.join(member);
				return new CreateMemberResponse(id);
		}

		@Data
		@AllArgsConstructor
		static class CreateMemberResponse {
				private Long id;
		}

		@Data
		@AllArgsContructor
		static class CreatememberRequest {
				private String name;
		}
}

 

 

 

응답값을 외부로 노출하지 말자

 

문제점

- 기본적으로 엔티티의 모든 값이 노출된다

- 응답 스펙을 위한 로직이 추가된다(@JsonIgnore등)

- 실무에서 회원 엔티티를 위한 다양한 API가 만들어지는데 각각의 API를 위한 모든 요청 요구사항을 담기 어렵다

- 엔티티가 변경되면 API스펙이 변한다

 

따라서 별도의 DTO로 결과를 반환하자

-> 모든 엔티티 노출과 @JsonIgnore은 정말  최악이다(api가 하나가 아니기 때문에)

 

 

 


지연 로딩과 조회 성능 최적화

 

엔티티 직접 노출하지말라고 앞서 얘기했다

 

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

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

        return all;
    }
}

 

order는 Member와 일대다, Delivery는 일대일 관계라고 하자

  • 일단 엔티티를 직접 노출하는 것은 앞서 설명했다.
  • order → Member, order → Address 로 지연로딩 된다
  • 따라서 실제 엔티티 대신에 프록시 객체가 존재하는데 jackson라이브러리는 기본적으로 프록시 객체를 json으로 어떻게 생성해야하는지 모른다 → 예외발생
  • Hibernate5JakartaModule을 통해 스프링 빈으로 등록하면 해결가능하지만 이런방식은 사용하지 말자 → 엔티티를 외부로 노출하는 것은 좋지 않다

★ 지연로딩을 피하기 위해 즉시로딩으로 설정하지 말자

→ 즉시로딩하면 연관관계가 필요없는 경우에도 항상 데이터를 조회해서 성능 문제가 발생한다

→ 항상 지연로딩으로 하고 성능 최적화가 필요하면 fetch join을 사용하자

 

 

 

 

지연로딩으로 쿼리 N번 호출

 

RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDTO> ordersV2() {
        List<Order> orders = orderRepository.findAll();
				List<SimpleOrderDTO> result = orders.streram().map(o -> new SimpleOrderDTO(o)).collect(Collectors.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+N+N번 실행된다

→ order 1번 조회

→ order 에서 member 지연로딩 N번조회

→ order 에서 delivery 지연로딩 N번조회

→ 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다

 

// Order 조회
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 limit ?

/*첫 번째 유저(userA)*/
/*SimpleOrderDto order.getMember().getName() 으로 LAZY 초기화*/
select
    member0_.member_id as member_i1_4_0_,
    member0_.city as city2_4_0_,
    member0_.street as street3_4_0_,
    member0_.zipcode as zipcode4_4_0_,
    member0_.name as name5_4_0_ 
from
    member member0_ 
where
    member0_.member_id=?

/*SimpleOrderDto order.getDelivery().getAddress() 으로 LAZY 초기화*/
select
    delivery0_.delivery_id as delivery1_2_0_,
    delivery0_.city as city2_2_0_,
    delivery0_.street as street3_2_0_,
    delivery0_.zipcode as zipcode4_2_0_,
    delivery0_.status as status5_2_0_ 
from
    delivery delivery0_ 
where
    delivery0_.delivery_id=?



/*두 번째 유저(userB)*/
/*SimpleOrderDto order.getMember().getName() 으로 LAZY 초기화*/
select
    member0_.member_id as member_i1_4_0_,
    member0_.city as city2_4_0_,
    member0_.street as street3_4_0_,
    member0_.zipcode as zipcode4_4_0_,
    member0_.name as name5_4_0_ 
from
    member member0_ 
where
    member0_.member_id=?


/*SimpleOrderDto order.getDelivery().getAddress() 으로 LAZY 초기화*/
select
    delivery0_.delivery_id as delivery1_2_0_,
    delivery0_.city as city2_2_0_,
    delivery0_.street as street3_2_0_,
    delivery0_.zipcode as zipcode4_2_0_,
    delivery0_.status as status5_2_0_ 
from
    delivery delivery0_ 
where
    delivery0_.delivery_id=?

 

 

 

 

페치 조인 최적화

 

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

    @GetMapping("/api/v3/simple-orders")
    public List<SimpleOrderDTO> ordersV3() {
        List<Order> orders = orderRepository.findAllWithMemberDelivery();
				List<SimpleOrderDTO> result = orders.streram().map(o -> new SimpleOrderDTO(o)).collect(Collectors.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();
				}
		}
}


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

 

 

엔티티를 페치조인을 사용해서 쿼리 1번에 조회

페치조인으로 이미 조회된 상태이므로 지연로딩 발생하지 않음

 

 

 

JPA에서 바로 DTO로 조회

 

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;
	private final OrderSimpleQueryRepository orderSimpleQueryRepository;

    @GetMapping("/api/v4/simple-orders")
    public List<OrderSimpleQueryDTO> ordersV4() {
        return orderSimpleQueryRepository.findOrderDTO();
    }
}



@Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {
		private final EntityManager em;

		public List<OrderSimpleQueryDTO> findOrderDTO() {
		        return em.createQuery("select new jpabook.jpashop.repository.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();
    }
}



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

    public OrderSimpleQueryDTO(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }
}

 

  • 일반적인 SQL사용할때처럼 원하는 값을 선택해서 조회
  • new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
  • select 절에서 원하는 데이터를 직접 선택함으로 DB에서 애플리케이션으로의 네트워크 용량이 최적화되긴하지만 성능에 영향을 주는 부분은 from절 이후임으로 생각보다 미비
  • 리포지토리의 재사용성이 떨어지고, API스펙에 맞춘 코드가 리포지토리에 들어가는 단점

 

정리

엔티티를 DTO로 변환하거나 바로 DTO로 조회하는 방법이 있는데 상황에 따라 더 나은 방법은 선택하면 된다

 

쿼리방식 선택 권장 순서

1. 우선 엔티티를 DTO로 변환하는 방법을 선택

2. 필요하면 페치조인으로 성능 최적화 → 대부분의 성능 이슈가 해결됨

3. 그래도 안되면 DTO로 직접 조회

4.최후의 방법은 JPA가 제공하는 네이티브 SQL, 스프링 JDBC Template를 사용해서 SQL직접 사용

 

 

 


컬렉션 조회 최적화

 

엔티티를 직접 노출하지말라고 앞서 얘기하였다

 

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

 

 

  • orderItem, item 관계를 직접 초기화하면 Hibernate5JakartaModule 설정에 의해 엔티티를 JSON으로 생성
  • 양방향 연관관계를 가지는 필드의 경우 무한 루프에 빠질 수 있어 한쪽에 @JsonIgnore어노테이션을 추가

 

DTO로 변환

 

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

    return collect;
}

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

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

@Getter
static class OrderItemDTO {

    private String itemName;
    private int orderPrice;
    private int count;

    public OrderItemDTO(OrderItem orderItem) {
        itemName = orderItem.getItem().getName();
        orderPrice = orderItem.getOrderPrice();
        count = orderItem.getCount();
    }

}

 

 

  • 지연로딩으로 인해 너무 많은 SQL문 발생
  • → order 1번, member/address 1번, orderItem N번, item N 번

 

페치조인으로 최적화하자

 

@GetMapping("/api/v3/orders")
public List<OrderDTO> ordersV3() { // hibernate6 부터 distinct 키워드 없이도 자동으로 중복 제거
    List<Order> orders = orderRepository.findAllWithItem();
    List<OrderDTO> collect = orders.stream().map(order -> new OrderDTO(order)).collect(toList());

    return collect;
}



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

 

  • 페치조인으로 sql이 1번만 실행된다
  • distinct를 사용한 이유는 1대 다 조인이 잇으므로 데이터베이스 row수가 증가한다 → 결과적으로 같은 order 엔티티 조회수도 증가한다 → distinct는 같은 엔티티를 조회하면 애플리케이션에서 중복을 걸려준다
  • Hibernate6 버전부터는 distinct 키워드를 사용하지 않아도 자동으로 중복을 제거해주는 기능이 생김
  • 그러나 컬렉션 fetch join는 페이징을 할 수 없다는 단점이 존재 → 컬렉션 fetch join을 페이징 시도하면 하이버네이트가 경고로그를 남기면서 모든 데이터를 db에서 읽어오고 메모리 에서 페이징함(메모리 부족 오류 발생)
  • 컬렉션 페치조인은 1개만 사용할 수 있다 → 두개이상 컬렉션에 페치조인을 사용하면 데이터가 부정합하게 조회될 수 있음
 

 

 

 페이징과 한계 돌파

 

컬렉션을 페치조인하면 페이징이 불가능하다

→ 페치조인하면 일대다 조인이 발생하기 때문에 데이터가 예측할 수 없이 증가

→ 일대다에서 일을 기준으로 페이징 하는 것이 목적이지만 데이터는 다를 기준으로 row가 생성됨

 

그렇다면 어떻게 해야할까??

  • 먼저 ToOne관계를 모두 패치조인한다. row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다
  •  컬렉션은 지연로딩으로 조회한다
  • 지연 로딩 최적화를 위해 hiberante.defafult_batch_fetch_size 또는 @BatchSize를 설정해준다 →이 옵션을 사용하면 컬렉션이나 프록시 객체를 한꺼번에 설정한 size만큼 IN쿼리로 조회한다

 

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


@GetMapping("/api/v3.1/orders")
public List<OrderDTO> ordersV3_page(@RequestParam(value = "offset", defaultValue = "0") int offset,
                                    @RequestParam(value = "limit", defaultValue = "100") int limit) {

    List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit);

    List<OrderDTO> collect = orders.stream().map(order -> new OrderDTO(order)).collect(toList());

    return collect;
}

 

 

## application.yml
spring: jpa:
        properties:
          hibernate:
            default_batch_fetch_size: 1000

 

개별로 설정하려면 @BatchSize를 적용하면 된다(컬렉션은 컬렉션 필드에, 엔티티는 엔티티 클래스에 적용)

 

  • 조인 보다 DB 데이터 전송량이 최적화 된다 → 중복 데이터 사라짐
  • 쿼리 호출수가 1+N → 1+1로 최적화 된다
  • 페치조인 방식과 비교해서 쿼리 호출수가 증가하지만 DB데이터 전송량이 감소한다

 

 

DTO 직접 조회

 

prviate final OrderQueryRepository orderQueryRepository;

@GetMapping("/api/v4/orders")
public List<OrderQueryDTO> ordersV4() {
    return orderQueryRepository.findOrderQueryDTO();
}


@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    public List<OrderQueryDTO> findOrderQueryDTO() {
    	//루트 조회(toOne 코드를 모두 한번에 조회)
        List<OrderQueryDTO> result = findOrders(); 
        
        //루프를 돌면서 컬렉션 추가(추가 쿼리 실행)
        result.forEach(o -> {
            List<OrderItemQueryDTO> orderItems = findOrderItems(o.getOrderId()); // 쿼리 N번
            o.setOrderItems(orderItems);
        });

        return result;
    }

    /**
    * 1:N 관계(컬렉션)를 제외한 나머지를 한번에 조회
    */
    private List<OrderItemQueryDTO> findOrderItems(Long orderId) {
        return em.createQuery(
                        "select new jpabook.jpashop.repository.order.query.OrderItemQueryDTO(oi.order.id, i.name, oi.orderPrice, oi.count) " +
                                "from OrderItem oi " +
                                "join oi.item i " +
                                "where oi.order.id = :orderId", OrderItemQueryDTO.class)
                .setParameter("orderId", orderId)
                .getResultList();
    }
}


@Data
@EqualsAndHashCode(of = "orderId")
public class OrderQueryDTO {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    private List<OrderItemQueryDTO> orderItems;

    public OrderQueryDTO(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }
}


@Data
public class OrderItemQueryDTO {

    @JsonIgnore
    private Long orderId;
    private String itemName;
    private int orderPrice;
    private int count;

    public OrderItemQueryDTO(Long orderId, String itemName, int orderPrice, int count) {
        this.orderId = orderId;
        this.itemName = itemName;
        this.orderPrice = orderPrice;
        this.count = count;
    }
}

 

  • ToOne 관계들은 먼저 조회하고 , ToMany관계는 각각 별도로 처리
  • ToOne관계는 조인해도 데이터 row수가 증가하지 않는다
  • ToMany 관계는 조인하면 row수가 증가한다

따라서 row수가 증가하지 않는 ToOne관계는 조인으로 최적화하기 쉬우므로 한번에 조회하고, ToMany관계는 최적화하기 어려우므로 별도의 메서드로 조회한다

 

 

JPA에서 DTO 직접 조회 - 컬렉션 조회 최적화

 

 

@GetMapping("/api/v5/orders")
public List<OrderQueryDTO> ordersV5() {
    return orderQueryRepository.findAllByDTO_optimization();
}


public List<OrderQueryDTO> findAllByDTO_optimization() {
	//루트 조회(toOne 코드를 모두 한번에 조회)
    List<OrderQueryDTO> result = findOrders();

    List<Long> orderIds = toOrderIds(result);

	//orderItem 컬렉션을 MAP 한방에 조회
    Map<Long, List<OrderItemQueryDTO>> orderItemMap = findOrderItemMap(orderIds);

    result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));

    return result;
}

private Map<Long, List<OrderItemQueryDTO>> findOrderItemMap(List<Long> orderIds) {
    List<OrderItemQueryDTO> orderItems = em.createQuery(
                    "select new jpabook.jpashop.repository.order.query.OrderItemQueryDTO(oi.order.id, i.name, oi.orderPrice, oi.count) " +
                            "from OrderItem oi " +
                            "join oi.item i " +
                            "where oi.order.id in :orderIds", OrderItemQueryDTO.class)
            .setParameter("orderIds", orderIds)
            .getResultList();

    Map<Long, List<OrderItemQueryDTO>> orderItemMap = orderItems
            .stream().collect(Collectors.groupingBy(orderItemQueryDTO -> orderItemQueryDTO.getOrderId()));
    return orderItemMap;
}

private List<Long> toOrderIds(List<OrderQueryDTO> result) {
    return result.stream().map(o -> o.getOrderId()).collect(Collectors.toList());
}

 

 

  • 루트 1번, 컬렉션 1번
  • ToOne관계들을 먼저 조회하고, 여기서 얻은 식별자로 orderId로 ToMany관계인 orderItem을 한꺼번에 조회
  • Map을 사용해서 매칭 성능 향상

 

JPA에서 DTO로 직접 조회 , 플랫 데이터 최적화

 

@GetMapping("/api/v6/orders")
public List<OrderQueryDTO> ordersV6() {
    List<OrderFlatDTO> flats = orderQueryRepository.findAllByDTO_flat();

    return flats.stream()
            .collect(groupingBy(o -> new OrderQueryDTO(o.getOrderId(),
                            o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
                    mapping(o -> new OrderItemQueryDTO(o.getOrderId(),
                            o.getItemName(), o.getOrderPrice(), o.getCount()), toList())
            )).entrySet().stream()
            .map(e -> new OrderQueryDTO(e.getKey().getOrderId(),
                    e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(),
                    e.getKey().getAddress(), e.getValue()))
            .collect(toList());
}



public OrderQueryDTO(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address, List<OrderItemQueryDTO> orderItems) {
    this.orderId = orderId;
    this.name = name;
    this.orderDate = orderDate;
    this.orderStatus = orderStatus;
    this.address = address;
    this.orderItems = orderItems;
}



public List<OrderFlatDTO> findAllByDTO_flat() {
    return em.createQuery(
                    "select new " +
                            "jpabook.jpashop.repository.order.query.OrderFlatDTO(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count) " +
                            "from Order o " +
                            "join o.member m " +
                            "join o.delivery d " +
                            "join o.orderItems oi " +
                            "join oi.item i", OrderFlatDTO.class)
            .getResultList();
}



@Data
public class OrderFlatDTO {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    private String itemName;
    private int orderPrice;
    private int count;

    public OrderFlatDTO(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address, String itemName, int orderPrice, int count) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
        this.itemName = itemName;
        this.orderPrice = orderPrice;
        this.count = count;
    }
}

 

 

  • 쿼리 1번 발생
  • 하지만 조인으로 인해 DB에서 애플리케이션에 전달하는 중복 데이터가 추가되므로 상황에따라 V5보다 더 느릴 수 ㅣㅆ음
  • 페이징 불가능하고 애플리케이션에서 추가 작업이 크다

 

 


API 개발 정리

 

1. 엔티티 조회

  • 엔티티를 조회해서 그대로 반환
  • 엔티티 조회후 DTO로 변환
  • 페치 조인으로 쿼리수 최적화
  • 컬렉션 페이징과 한계 돌파  → 컬렉션은 페치 조인시 페이징 불가능 / ToOne관계는 페치조인으로 쿼리수 최적화 / 컬렉션은 페치조인대신 지연로딩으로 유지하고 hibernate.default_batch_fetch_size, @BatchSize로 최적화

 

2. DTO 직접조회

  • JPA에서 DTO 직접 조회
  • 컬렉션 조회 최적화 → 일대다 관계인 컬렉션은 IN절을 활용해서 메모리에 미리 조회해서 최적화
  • 플랫 데이터 최적화 → JOIN결과를 그대로 조회후 애플리케이션에서 원하는 모양으로 직접 변환

 

권장순서

일반적으로 엔티티 조회방식으로 우선 접근하여 페치조인으로 쿼리 수를 최적화하고, 컬렉션을 최적화한다

엔티티 조회방식으로 해결이 안될 경우 DTO조회 방식을 사용하고, 그래도 안될 경우 네이티브 SQL이나 스프링 JdbcTemplate을 사용한다

 

참고: 엔티티 조회 방식은 페치 조인이나, hibernate.default_batch_fetch_size , @BatchSize 같이 코드를 거의 수정하지 않고, 옵  션만 약간 변경해서, 다양한 성능 최적화를 시도할 수 있다. 반면에 DTO를 직 접 조회하는 방식은 성능을 최적화 하거나 성능 최적화 방식을 변경할 때 많은 코드를 변경해야 한다

 

 

 

 


실무 필수 최적화

 

OSIV와 성능 최적화

 

OSIV는 스프링에서 기본적으로 on 상태로 돌아간다

application.yml파일에 spring.jpa.open-in-view : true 을 통해 지정가능

 

 

OSIV 옵션이 켜져있으면 트랜젝션 시작처럼 최초 데이터베이스 커넥션 시작지점부터 API응답이 끝날때까지 영속성 컨텍슽와 데이터베이스 커넥션을 유지한다. 그래서 지금까지 뷰 템플릿이나 API 컨트롤러에서 지연 로딩이 가능했던 것이다.

 

지연로딩은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지한다

 

그런데 너무 오랜시간동안 데이터베이스 커넥션 리소스를 사용하기때문에, 실시간 트랙픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다  → 장애로 이어짐 (예를 들어 컨트롤러에서 외부 API를 호출하면 외부 API 대기 시간 만큼 터넥션 리소스를 반환하지 못하고, 유지해야함)

 

 

 

 

OSIV를 끄면 트랜젝션을 종료할때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소스를 낭비하지 않음

 

그러나 OSIV를 끄면 모든 지연로딩을 트랜젝션 안에서 처리해야함으로 지금까지 작성한 많은 지연로딩 코드를 트랜젝션안으로 넣어야한다. view template에서 지연로딩이 동작하지 않는다. 

 

결론적으로 트랜젝션이 끝나기 전에 지연로딩을 강제로 호출해두어야한다

 

 

 

커맨드와 쿼리 분리

 

실무에서 OSIV를 끈 상태로 복잡성을 관리하는 좋은 방법은 Command와 Query를 분리하는 방법이다

보통 비지니스 로직은 특정 엔티티 몇개를 등록하거나 수정하는 것이므로 성능이 크게 문제가 되지 않는다

그런데 복잡한 화면을 출력하기 위한 쿼리는 화면에 맞추어 성능을 최적화하는 것이 중요하다. 하지만 그 복작서에 비해 핵심 비지니스에 큰 영향을 주는 것은 아니다.

 

그래서 크고 복잡한 애플리케이션을 개발한다면, 이 둘의 관심사를 명확하게 분리하는 선택은 유지보수 관점에서 충분히 의미가 있다

 

예를 들어

 

 

 

★ 참고로 인강을 제공해주신 분은 고객 서비스의 실시간 API 는 OSIV를 끄고, ADMIN처럼 커넥션을 많이 사용하지 않는 곳에 OSIV를 켠다

 

 

 

 

 

 

 

 

이 모든 내용은 인프런 _ 실전! 스프링 부트와 JPA 활용2 - API개발과 성능최적화를 참고하였습니다.