[JPA] API 개발하기 - 1

yrok·2023년 10월 9일

JPA로 API 개발하기

목록 보기
1/1

📝 엔티티를 외부에 노출하지말자.

@GetMapping("/api/v1/members")
public List<Member> membersV1() {
	return memberService.findMembers();
}

위와 같이 코드를 작성하면 응답 값으로 엔티티를 직접 외부에 노출하게된다. 이는 다음과 같은 문제를 야기한다.

  • 기본적으로 엔티티의 모든 값이 노출된다. ( 필요하지 않은 필드도 노출 )
  • 응답 스펙을 맞추기 위해 로직이 추가된다. ( @JsonIgnore 등 )
  • 수많은 API가 용도에 따라 다양하게 만들어지는데, 하나의 엔티티로 처리하기 어렵다.
  • 엔티티가 변경되면 API 스펙이 변한다.
  • 응답으로 컬렉션을 직접 반환하면 향후 API 스펙을 변경하기 어렵다.

문제를 해결하기 위해 API 응답 스펙에 맞춰 별도의 DTO를 반환

@GetMapping("api/v2/members")
public Result membersV2() {

	List<Member> findMembers = memberService.findMembers();
    List<MemberDto> collect = findMembers.stream()
    		.map(m -> new MemberDto(m.getName())
            .collect(Collectors.toList());
    
    return new Result(collect.size(), collect);
}


@Data
@AllArgsConstructor
static class Result<T> {
	
    private T data;
    private int count;
}

@Data
@AllArgsConstructor
static class MemberDto {
	private String name;
}

  • 기존 엔티티의 모든 값을 노출하던 응답과 달리 MemberDto에 지정한 name 필드만 응답으로 출력하고 있다.
  • 컬렉션을 그대로 응답으로 보낸다면 차후에 API 스펙을 변경할 때 문제가 되기 때문에 Result에 담아서 오른쪽과 같이 출력한다면 향후 필요한 필드를 추가할 수 있다.
  • 엔티티가 변경되어도 API 스펙이 변경되지 않는다.

📝 엔티티를 DTO로 변환할 때 N+1 문제 해결

📌 N+1 문제란 ?

연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우 조회된 데이터 갯수(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(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. orders를 가져오는 select 쿼리가 1번 호출된다.
  2. stream에서 각 order를 돌며 SimpleOrderDto를 생성한다.
  3. 이 때, name과 address는 영속성 컨텍스트에 저장되어 있지 않기에 getMember(), getDelivery()에서 select 쿼리가 호출된다. -> 1개의 order에서 2개의 select 쿼리 호출
  4. 최악의 경우, SimpleOrderDto 내부에 있는 영속성 컨텍스트에 등록되어있지 않은 필드가 N개라면 총 쿼리의 개수는 1 + orders.size()*N 이다.

📌 fetch join으로 해결

@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
  
  List<Order> orders = orderRepository.findAllWithMemberDelivery();
  return orders.stream()
  		.map(o -> new SimpleOrderDto(o))
		.collect(Collectors.toList());
}
// orderRepository
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();
}
  • 엔티티를 fetch join을 사용해서 쿼리 1번만에 조회
  • order -> member, order -> delivery는 이미 조회된 상태이므로 지연로딩 X

    왼쪽 그림에서 발생한 N+1 문제를 오른쪽 그림과 같이 fetch join을 사용해 1개의 쿼리로 호출해서 해결할 수 있다.
profile
공부 일기장

0개의 댓글