jpa-spring boot - api 실습 (2)

김강현·2023년 4월 12일
0

SPRING-JPA-실습-API

목록 보기
2/4

조회용 샘플 데이터 입력

@Component
@RequiredArgsConstructor
public class InitDb {

    private final InitService initService;

    @PostConstruct
    public void init(){
        initService.dbInit1();
        initService.dbInit2();
    }

    @Component
    @Transactional
    @RequiredArgsConstructor
    static class InitService {
        private final EntityManager em;
        public void dbInit1(){
            Member member = getMemberWidthNameAndAddressProps("user A", "서울", "1", "1245");
            em.persist(member);

            Book book1 = getBookWidthNamePriceStock("JPA1 bOOK", 10000, 100);
            em.persist(book1);

            Book book2 = getBookWidthNamePriceStock("JPA2 bOOK", 20000, 200);
            em.persist(book2);

            OrderItem orderItem1 = OrderItem.createOrderItem(book1, 10000, 1);
            OrderItem orderItem2 = OrderItem.createOrderItem(book2, 20000, 2);

            Delivery delivery = new Delivery();
            delivery.setAddress(member.getAddress());
            Order order = Order.createOrder(member, delivery, orderItem1, orderItem2);
            em.persist(order);
        }
        public void dbInit2(){
        	...
        }


        private static Member getMemberWidthNameAndAddressProps(String name, String cityName, String street, String zipcode) {
            ...
            return member;
        }

        private static Book getBookWidthNamePriceStock(String name, int price, int stock) {
        	...
            return book;
        }

    }
}

Spring 이 실행되면서, @PostConstruct 의 함수를 먼저 실행함!
이때 목업/샘플 데이터를 주입해주면 됨!!

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

지금부터 설명하는 내용은 정말 중요!!
대충 넘어가면 엄청난 시간을 날리고 인생을 허비하게 될 것임!! feat. 김영한 개발자님

simple order 조회 v1

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

무난하게 List<Order> 를 json 파일로 바꿔줄 것 같은 식이다!
하지만!! json 으로 바꿔주는 과정에서,

<Order.java>
	@ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

member를 타고

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

여기서 orders를 호출하여 이를 다시 json 화 시키는데,
이때 무한 루프가 발생한다!!
양방향 연관관계에서의 문제가 생김!

해결

@JsonIgnore 을 걸어주면 된다!
양방향 중에 한쪽은 무조건 걸어줘야함!! (일대다, 일대일 이던, 양방향이라면)
Member OrderItem Delivery 다 바꿔주기



또 다른 이슈

class org.hibernate.proxy.poho.bytebuddy.ByteBuddyInterceptor 하는 친구가 뜬다.
바로, lazy loading 에 걸려있는 객체들이 json화 되는 과정에서 문제가 된 것!

  • 지연 로딩인 경우, 그냥 뿌리지 말라고 지정해줄 수 있음!
  • 해당 라이브러리를 다운로드 받아야함. (스프링부트가 알아서 버전 맞춰줌)
<build.gradle>
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5' // 추가
<mainController> 추가
@Bean
Hibernate5Module hibernate5Module() {
	return new Hibernate5Module();
}

위 처럼, lazy loading 데이터들은 null로 넘어온다!
lazy 로딩을 억지로 불러와서 data를 강제할 수 있는 속성도 추가할 수 있음!
hibernate5Module.configure(Hibernate5Module.Featrue.FORCE_LAZY_LOADING, true);

엔티티를 직접 반환하는 api 는 여러모로 제약이 많다.
json화, lazy loading 등 여러가지를 신경써줘야함.
1. 왠만하면 DTO 로 변환해서 반환하자!!
2. lazy loading 때문에, eager 로 바꾸는 것이 아니라, 필요한 경우 페치 조인으로 ㄱㄱ

덧) 반환 값은 리스트로 보내는 일이 없도록하자!
추가적으로 필드 값을 수정할때, 확장이 용이 하지 않음

<newField 추가 하기 쉽네~>
{
   newField: ~~~,
   data: [ ... ]
}
<newField 어디에 추가하지..?>
[
  ...
]

simple order 조회 v2

v1 에서의 엔티티 직접 반환을 해결하여, DTO를 세팅하면 아래와 같다.

    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2() {
        List<Order> orders = orderRepository.findAll(new OrderSearch());

        List<SimpleOrderDto> orderDtoList = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());

        return orderDtoList;
    }

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

        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;
    }

한계

lazy loading 으로 인한, 엔티티 개별로 query 호출!! (영속성 컨텍스트로 호출이긴 함)
n + 1 문제
그렇다고 Lazy -> Eager로 바꾸면? NoNo!
Fetch Join 튜닝!! ㄱㄱ

simple order 조회 v3

< OrderRespository.java >

    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 을 활용하여, 애초에 값을 다 가지고오기!
JPA 가 고맙게도 쿼리를 한방에 작성해서 날려줌

100% 이해할 것!!!! 거의 이게 성능의 90퍼센트 임!!!

<OrderSimpleApiController.java>
    @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(Collectors.toList());

        return result;
    }

v2 와 결과는 같지만! 실행하는 Query 개수가 다름!!

simple order 조회 v4

JPA 에서 바로 DTO 로 조회!!

<OrderSimpleApiController.java>
    @GetMapping("/api/v4/simple-orders")
    public List<OrderSimpleQueryDto> ordersV4() {
        return orderRepository.findOrderDtos();
    }
<OrderRepository.java>
    public List<OrderSimpleQueryDto> findOrderDtos(){
        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();
    }
<OrderSimpleQueryDto.java>
@Data
public class OrderSimpleQueryDto {
    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;
    }

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
}

차이점이라 하면!

Entity에 종속되어 DTO를 만들지 않고,
Entity와 별개로 원하는 것만 얻어오는 DTO 를 만들 수 있다는 것!!
(member 나 delivery 상세 정보 같은 것들 생략 가능)

  • 하지만 재사용성이 떨어짐! 확장성은 거의 없기 때문에

V3 과 V4 는 Trade-Off 가 있음.
가급적 V3 를 추천!!
너무나도 많은 Traffic을 다루는 거라면, 해당 프로세스에 맞게 V4 방식으로 고치면 됨.

김영한 개발자님 실무 팁

repository 에 query api 스펙이 포함된 DTO 파일을 관리하는게, 뭔가 애매 (+ repository 의 return 값이 순수하지 않음)
query 용 repository 를 따로 만드는 것으로 하는 편

<OrderSimpleQueryRepository.java>
@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();
    }
}
<OrderSimpleApiController.java>
	...
    private final OrderSimpleQueryRepository orderSimpleQueryRepository;
    ...    
    @GetMapping("/api/v4/simple-orders")
    public List<OrderSimpleQueryDto> ordersV4() {
        return orderSimpleQueryRepository.findOrderDtos();
    }
    ...

이런 식으로 새로운 repository 를 추가해서, Controller 나 Service 계층에서 사용하는 방식을 선호하심!! (V4의 경우)

정리

조회 방식
1. 엔티티로 받아 DTO로 변환 후 전달 (V3)
-- 리포지토리 재사용성 좋음
-- 개발이 단순해짐
-- fetch join 활용 가능

  1. DTO로 받아 그대로 전달 (V4)
    -- 하게 된다면!
    -- 새로운 독립된 repository 를 만들어서, 주입시키는 것을 추천!
profile
this too shall pass

0개의 댓글