Search

스프링 부트와 JPA 활용 - API

등록 API

V1 : 엔티티를 Request Body에 직접 매핑
//Controller @RestController @RequiredArgsConstructor public class MemberApiController { private final MemberService memberService; @PostMapping("/api/v1/members") public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member) { Long id = memberService.join(member); return new CreateMemberResponse(id); } @Data static class CreateMemberResponse { private Long id; public CreateMemberResponse(Long id) { this.id = id; } } }
Java
복사
//Entity @Entity @Getter @Setter public class Member { @Id @GeneratedValue @Column(name = "member_id") private Long id; @NotEmpty private String name; @Embedded private Address address; @JsonIgnore @OneToMany(mappedBy = "member") private List<Order> orders = new ArrayList<>(); }
Java
복사
문제점
엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
엔티티에 API 검증을 위한 로직이 들어간다. (@NotEmpty 등등)
실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한
모든 요청 요구사항을 담기는 어렵다.
엔티티가 변경되면 API 스펙이 변한다.
결론
API 요청 스펙에 맞추어 별도의 DTO를 파라미터로 받는다.
V2 : 엔티티 대신에 DTO를 RequestBody에 매핑
//Controller @RestController @RequiredArgsConstructor public class MemberApiController { private final MemberService memberService; @PostMapping("/api/v2/members") public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) { Member member = new Member(); member.setName(request.getName()); Long id = memberService.join(member); return new CreateMemberResponse(id); } @Data static class CreateMemberRequest { private String name; } @Data static class CreateMemberResponse { private Long id; public CreateMemberResponse(Long id) { this.id = id; } } }
Java
복사
CreateMemberRequest 를 Member 엔티티 대신에 RequestBody와 매핑한다.
엔티티와 프레젠테이션 계층을 위한 로직을 분리할 수 있다.
엔티티와 API 스펙을 명확하게 분리할 수 있다.
엔티티가 변해도 API 스펙이 변하지 않는다.
→ 절대로 Entity를 요청, 응답에 사용하면 안된다. 대신 DTO를 사용하자.

수정 API

PUT : 전체 업데이트 할 때
PATCH, POST : 부분 업데이트 할 때
//Controller @RestController @RequiredArgsConstructor public class MemberApiController { private final MemberService memberService; @PostMapping("/api/v2/members/{id}") public UpdateMemberResponse updateMemberV2(@PathVariable("id") Long id, @RequestBody @Valid UpdateMemberRequest request) { memberService.update(id, request.getName()); Member member = memberService.findOne(id); return new UpdateMemberResponse(member.getId(), member.getName()); } //Request DTO @Data static class UpdateMemberRequest { private String name; } //Response DTO @Data @AllArgsConstructor static class UpdateMemberResponse { private Long id; private String name; } }
Java
복사
//Service @Service @Transactional(readOnly = true) @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; @Transactional public void update(Long id, String name) { Member member = memberRepository.findOne(id); member.setName(name); } }
Java
복사
JPA의 변경감지를 이용해 데이터를 수정한다.
수정도 마찬가지로 DTO를 이용해 매핑한다.

조회 API

application.yml 의 jpa:hibernate:ddl-auto:
none : 테이블을 만들지 않고 이미 만들어진 테이블을 드랍하지 않는다.
create : 실행마다 테이블을 만들고 종료 시 테이블을 드랍한다.
V1 : 엔티티를 직접 외부에 노출
//Controller @RestController @RequiredArgsConstructor public class MemberApiController { private final MemberService memberService; @GetMapping("/api/v1/members") public List<Member> membersV1() { return memberService.findMembers(); } }
Java
복사
//Entity @Entity @Getter @Setter public class Member { @Id @GeneratedValue @Column(name = "member_id") private Long id; @NotEmpty private String name; @Embedded private Address address; @JsonIgnore @OneToMany(mappedBy = "member") private List<Order> orders = new ArrayList<>(); }
Java
복사
@JsonIgnore
Entity를 JSON으로 직접 내릴 때 해당 프로퍼티에 대한 데이터를 포함하지 않는다.
문제점
엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
기본적으로 엔티티의 모든 값이 노출된다.
응답 스펙을 맞추기 위해 로직이 추가된다. (@JsonIgnore, 별도의 뷰 로직 등등)
실무에서는 같은 엔티티에 대해 API가 용도에 따라 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한 프레젠테이션 응답 로직을 담기는 어렵다.
엔티티가 변경되면 API 스펙이 변한다.
추가로 컬렉션을 직접 반환하면 항후 API 스펙을 변경하기 어렵다.(별도의 Result 클래스 생성으로 해결)
결론
API 응답 스펙에 맞추어 별도의 DTO를 반환한다.
V2 : 엔티티 대신 DTO를 외부에 노출
//Controller @RestController @RequiredArgsConstructor public class MemberApiController { private final MemberService memberService; @GetMapping("/api/v2/members") public Result memberV2() { List<Member> members = memberService.findMembers(); List<MemberDto> collect = members.stream() .map(m -> new MemberDto(m.getName())) .collect(Collectors.toList()); return new Result(collect.size(), collect); } //wrapper class @Data @AllArgsConstructor static class Result<T> { private int count; private T data; } //DTO @Data @AllArgsConstructor static class MemberDto { private String name; } }
Java
복사
엔티티를 DTO로 변환해서 반환한다.
엔티티가 변해도 API 스펙이 변경되지 않는다.
추가로 Result 클래스로 컬렉션을 감싸면 향후 필요한 필드를 추가할 수 있다.

더미 데이터

@Component @RequiredArgsConstructor public class InitDb { private final InitService initService; @PostConstruct public void init() { initService.dbInit1(); initService.dbInit2(); } }
Java
복사
@PostConstruct를 이용해서 애플리케이션 실행 후 init() 를 호출
@Component @Transactional @RequiredArgsConstructor static class InitService { private final EntityManager em; public void dbInit1() { System.out.println("Init1" + this.getClass()); Member member = createMember("userA", "서울", "1", "1111"); em.persist(member); Book book1 = createBook("JPA1 BOOK", 10000, 100); em.persist(book1); Book book2 = createBook("JPA2 BOOK", 20000, 100); em.persist(book2); OrderItem orderItem1 = OrderItem.createOrderItem(book1, 10000, 1); OrderItem orderItem2 = OrderItem.createOrderItem(book2, 20000, 2); Delivery delivery = createDelivery(member); Order order = Order.createOrder(member, delivery, orderItem1, orderItem2); em.persist(order); } public void dbInit2() { Member member = createMember("userB", "진주", "2", "2222"); em.persist(member); Book book1 = createBook("SPRING1 BOOK", 20000, 200); em.persist(book1); Book book2 = createBook("SPRING2 BOOK", 40000, 300); em.persist(book2); OrderItem orderItem1 = OrderItem.createOrderItem(book1, 20000, 3); OrderItem orderItem2 = OrderItem.createOrderItem(book2, 40000, 4); Delivery delivery = createDelivery(member); Order order = Order.createOrder(member, delivery, orderItem1, orderItem2); em.persist(order); } private Member createMember(String name, String city, String street, String zipcode) { Member member = new Member(); member.setName(name); member.setAddress(new Address(city, street, zipcode)); return member; } private Book createBook(String name, int price, int stockQuantity) { Book book1 = new Book(); book1.setName(name); book1.setPrice(price); book1.setStockQuantity(stockQuantity); return book1; } private Delivery createDelivery(Member member) { Delivery delivery = new Delivery(); delivery.setAddress(member.getAddress()); return delivery; } }
Java
복사
JPA를 통해 DB에 접근해야 하기 때문에 @Transactional을 건다.
간단하게 더미데이터를 추가하는 로직이기 때문에 EntityManager를 바로 주입 받는다.

지연 로딩과 조회 성능 최적화 (OneToOne, ManyToOne)

지연 로딩
//Order.java @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; @JsonIgnore @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) private List<OrderItem> orderItems = new ArrayList<>(); @JsonIgnore @OneToOne(fetch = LAZY, cascade = CascadeType.ALL) @JoinColumn(name = "delivery_id") private Delivery delivery; private LocalDateTime orderDate; //주문시간 @Enumerated(EnumType.STRING) private OrderStatus status; //주문상태 [ORDER, CANCEL] }
Java
복사
Order를 조회할 때 연관관계 매핑된 member, delivery 또한 함께 조회된다.
fetch = LAZY, EAGER를 통해 로딩 방식을 정할 수 있다.
EAGER (즉시 로딩) : Order를 조회할 때 member, delivery도 조회하는 SQL을 한꺼번에 날린다.
LAZY (지연 로딩) : Order를 조회할 땐 member, delivery를 조회하지 않고 사용하게 되면 조회.
(order.getMember().getName() 을 호출하면 getName()을 얻기 위해 LAZY를 강제 초기화)
@xxToOne 의 기본값은 EAGER, @xxToMany의 기본값은 LAZY이다.
무한 루프
//Member.java @Entity @Getter @Setter public class Member { @Id @GeneratedValue @Column(name = "member_id") private Long id; @NotEmpty private String name; @Embedded private Address address; @JsonIgnore @OneToMany(mappedBy = "member") private List<Order> orders = new ArrayList<>(); }
Java
복사
Order를 조회하면 Order의 member를 조회하기 위해 Member를 조회하고 Member의 orders를 조회하기 위해 Order를 다시 조회하며 무한 루프에 빠지게 된다.
→ Entity를 직접 사용하지 말고 DTO를 사용한다.
→ @JsonIgnore, @JsonManagedReference, @JsonBackReference, @JsonIdentityInfo 를 사용하여 직렬화, 역직렬화를 제어한다.
N + 1 문제
//DTO @Data public class SimpleOrderQueryDto { private Long orderId; private String name; private LocalDateTime orderDate; private OrderStatus orderStatus; private Address address; public SimpleOrderQueryDto(Order order) { orderId = order.getId(); name = order.getMember().getName(); orderDate = order.getOrderDate(); orderStatus = order.getStatus(); address = order.getDelivery().getAddress(); } } //Controller @GetMapping("/api/v2/simple-orders") public List<SimpleOrderQueryDto> ordersV2() { return orderRepository.findAllByString(new OrderSearch()).stream().map(SimpleOrderQueryDto::new).collect(Collectors.toList()); }
Java
복사
Entity를 DTO로 변환해서 사용하더라도 N + 1 문제가 생긴다.
Order 1번 조회 : 결과 수 N개
order → member 조회 : 지연 로딩 조회 N번
order → delivery 조회 : 지연 로딩 조회 N번
Order 조회 결과가 4개라면 1번 조회에 1 + 4 + 4 번의 SQL이 날라간다.
//Controller @GetMapping("/api/v3/simple-orders") public List<SimpleOrderQueryDto> orderV3() { return orderRepository.findAllWithMemberDelivery().stream().map(SimpleOrderQueryDto::new).collect(Collectors.toList()); } //Repository 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(); }
Java
복사
fetch join을 이용하여 한방 쿼리를 만들어 N + 1 문제를 해결할 수 있다.
join : 질의 대상 Entity에 대한 컬럼만 SELECT
fetch join : 질의 대상 Entity + fetch join 대상 Entity 의 컬럼을 모두 SELECT
(두 Entity의 컬럼을 다 붙여서 하나의 Row로 가져오기 때문에 LAZY 로딩이 없다.)
DTO 변환 없이 직접 조회
//DTO @Data public class SimpleOrderQueryDto { private Long orderId; private String name; private LocalDateTime orderDate; private OrderStatus orderStatus; private Address address; public SimpleOrderQueryDto(Order order) { orderId = order.getId(); name = order.getMember().getName(); orderDate = order.getOrderDate(); orderStatus = order.getStatus(); address = order.getDelivery().getAddress(); } public SimpleOrderQueryDto(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; } } //Repository public List<SimpleOrderQueryDto> findOrderDtos() { return em.createQuery("select new jpabook.jpashop.repository.SimpleOrderQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" + " from Order o" + " join o.member m" + " join o.delivery d", SimpleOrderQueryDto.class) .getResultList(); }
Java
복사
new를 통해 JPQL의 결과를 DTO로 즉시 변환하며 원하는 값을 선택해서 조회할 수 있다.
레포지토리 재사용성이 떨어지고 API 스펙에 맞춘 코드가 레포지토리에 들어간다.

컬렉션 조회 최적화 (OneToMany)

Entity 직접 노출
//Controller @GetMapping("/api/v1/orders") public List<Order> ordersV1() { List<Order> all = orderRepository.findAll(); for (Order order : all) { order.getMember().getName(); order.getDelivery().getAddress(); List<OrderItem> orderItems = order.getOrderItems(); orderItems.stream().forEach(o -> o.getItem().getName()); } return all; } //Entity @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; private LocalDateTime orderDate; //주문시간 @Enumerated(EnumType.STRING) private OrderStatus status; //주문상태 [ORDER, CANCEL] }
Java
복사
findAll()을 통해 order 컬렉션을 받아온 후 양방향 연관관계에 대해 LAZY 강제 초기화를 시켜준다.
무한 루프에 걸리지 않게 한 곳에 @JsonIgnore를 추가해야 한다.
Entity를 DTO로 변환
//Controller @GetMapping("/api/v2/orders") public List<OrderDto> ordersV2() { List<Order> orders = orderRepository.findAllByString(new OrderSearch()); return orders.stream().map(OrderDto::new).collect(Collectors.toList()); } //DTO @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(OrderItemDto::new).collect(Collectors.toList()); } } @Data 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(); } }
Java
복사
Entity를 직접 노출하는건 좋지 않기 때문에 DTO로 변환해서 사용하지만 지연 로딩으로 인해 많은 양의 SQL이 날아가게 된다.
fetch join
//Controller @GetMapping("/api/v3/orders") public List<OrderDto> ordersV3() { return orderRepository.findAllWithItem().stream().map(OrderDto::new).collect(Collectors.toList()); } //Repository 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(); }
Java
복사
fetch join으로 여러 테이블의 컬럼을 엮어 한 번의 SQL만 날아간다.
join의 특성상 1이 N을 따라간다. (order는 1개고 orderItem 이 2개인 경우 order가 2개 조회)
→ distinct를 사용해 중복되는 order를 제거할 수 있다. (DB에도 날아가지만 DB는 모든 컬럼이 동일해야 중복으로 인식)
컬렉션 fetch join은 1개만 사용할 수 있다. 2개 이상의 컬렉션에 fetch join을 사용할 경우 데이터가 부정합하게 조회될 수 있다.
페이징과 한계 돌파
컬렉션에 fetch join을 사용할 경우 페이징이 불가능하다.
하이버네이트가 DB에서 모든 데이터를 조회해 메모리 상에서 페이징하기 때문에 위험하다.
컬렉션을 fetch join할 경우 일대다 조인이 발생하므로 데이터가 예측할 수 없이 증가한다.
1:N에서 1을 기준으로 페이징해야 하는데 데이터는 N을 기준으로 Row가 생성된다.
(1인 Order를 기준으로 페이징해야 하는데 N인 OrderItem을 기준으로 Row가 생성된다)
//Controller @GetMapping("/api/v3.1/orders") public List<OrderDto> ordersV3_page(@RequestParam(value = "offset", defaultValue = "0") int offset, @RequestParam(value = "limit", defaultValue = "100") int limit) { return orderRepository.findAllWithMemberDelivery(offset, limit).stream().map(OrderDto::new).collect(Collectors.toList()); } //Repository 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(); }
Java
복사
컬렉션을 페이징하는 방법
xxToOne 관계를 모두 fetch join한다. (xxToOne 관계는 Row수를 증가시키지 않는다.)
컬렉션은 지연 로딩으로 조회한다.
지연 로딩의 성능 최적화를 위해 Batch를 적용한다.
(hinernate.default_batch_fetch_size : 글로벌 설정 , @BatchSize 개별 설정)
Batch란
SQL의 IN절을 사용한다.
100~1000의 사이즈를 정해야 한다. (IN 파라미터의 상한선과 순간 부하를 감당할 수 있는 정도)
BatchSize만큼 WHERE절이 같은 여러 개의 SELECT 쿼리를 하나의 IN 쿼리로 만들어준다.
(BatchSize가 100이고 Row가 200개라면 SQL이 2개 날아간다.)
결국 전체 데이터를 로딩해야 하므로 메모리 사용량은 같다.
Batch의 장점
쿼리 호출 수가 1 + N 에서 1 + 1 로 최적화된다.
join보다 데이터 전송량이 최적화 된다
(join은 1:N의 경우 1이 N만큼 중복되어 조회되지만 이 방법은 각각 조회하므로 중복이 없다.)
fetch join 방식과 비교해서 쿼리 호출 수가 증가하지만 DB 데이터 전송량이 감소한다.
컬렉션 fetch join은 페이징이 불가능하지만 이 방법은 페이징이 가능하다.
컬렉션 DTO 사용해 직접 조회
1:N이 아닌 루트 SQL(ToOne)은 1회, 1:N인 컬렉션(ToMany)은 N회
ToOne 관계는 조인해도 데이터 row 수가 증가하지 않는다
ToMany 관계는 조인하면 row 수가 증가한다.
단건 조회에서 많이 사용하는 방식
//Controller @GetMapping("/api/v4/orders") public List<OrderQueryDto> orderV4() { return orderQueryRepository.findOrderQueryDtos(); } //Repository public List<OrderQueryDto> findOrderQueryDtos() { List<OrderQueryDto> result = findOrders(); //루트 SQL result.forEach(o -> { List<OrderItemQeuryDto> orderItems = findOrderItems(o.getOrderId()); //컬렉션 SQL o.setOrderItems(orderItems); }); return result; } public List<OrderItemQeuryDto> findOrderItems(Long orderId) { return em.createQuery("select new jpabook.jpashop.repository.order.query.OrderItemQeuryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" + " from OrderItem oi" + " join oi.item i" + " where oi.order.id = :orderId", OrderItemQeuryDto.class) .setParameter("orderId", orderId) .getResultList(); } public List<OrderQueryDto> findOrders() { return em.createQuery("select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" + " from Order o" + " join o.member m" + " join o.delivery d", OrderQueryDto.class) .getResultList(); }
Java
복사
컬렉션 DTO 사용해 직접 조회 - 최적화
1:N이 아닌 루트 SQL(ToOne)은 1회, 1:N인 컬렉션(ToMany)은 1회
ToOne 관계들을 먼저 조회하고 여기서 얻은 식별자(orderId)로 ToMany 를 한꺼번에 조회
Map을 사용해서 매칭 성능 향상 (O(1))
//Controller @GetMapping("/api/v5/orders") public List<OrderQueryDto> orderV5() { return orderQueryRepository.findAllByDto_optimization(); } //Repository public List<OrderQueryDto> findAllByDto_optimization() { List<OrderQueryDto> result = findOrders(); List<Long> orderIds = result.stream().map(o -> o.getOrderId()).collect(Collectors.toList()); List<OrderItemQeuryDto> orderItems = em.createQuery("select new jpabook.jpashop.repository.order.query.OrderItemQeuryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" + " from OrderItem oi" + " join oi.item i" + " where oi.order.id in :orderIds", OrderItemQeuryDto.class) .setParameter("orderIds", orderIds) .getResultList(); Map<Long, List<OrderItemQeuryDto>> orderItemMap = orderItems.stream().collect(Collectors.groupingBy(orderItemQeuryDto -> orderItemQeuryDto.getOrderId())); result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId()))); return result; } //DTO @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; } }
Java
복사
컬렉션 DTO 사용해 직접 조회 - 플랫 데이터 최적화
장점
DTO에 모든 데이터를 다 때려넣고 SQL 1번으로 모든 데이터를 다 받아온 후 stream을 이용해서 데이터 정제 후 반환
단점
쿼리는 한 번이지만 조인으로 인해 DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되므로 상황에 따라 더 느릴 수도 있다.
애플리케이션에서 추가 작업이 크다.
페이징 불가능
//Controller @GetMapping("/api/v6/orders") public List<OrderQueryDto> orderV6() { List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat(); return flats.stream() .collect(Collectors.groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()), Collectors.mapping(o -> new OrderItemQeuryDto(o.getOrderId(), o.getItemName(), o.getOrderPrice(), o.getCount()), Collectors.toList()))) .entrySet().stream() .map(e -> new OrderQueryDto(e.getKey().getOrderId(), e.getKey().getName(), e.getKey().getOrderData(), e.getKey().getOrderStatus(), e.getKey().getAddress(), e.getValue())) .collect(Collectors.toList()); } //Repository 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(); } //DTO @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; } }
Java
복사

쿼리 방식 선택 권장 순서

1.
Entity 조회 방식으로 우선 접근
a.
fetch join으로 쿼리 수를 최적화
b.
컬렉션 최적화
i.
페이징 필요 O : hibernate.default_batch_fetch_size, @BatchSize로 최적화
ii.
페이징 필요 X : fetch join 사용
2.
Entity 조회 방식으로 해결되지 않는다면 DTO로 변환하는 방식 사용
3.
그래도 안되면 DTO 직접 조회
a.
쿼리가 1번 실행된다고 무조건 좋은 방법이 아니다.
b.
코드가 단순하면서 성능이 좋아야 한다.
4.
최후의 방법으로 Native SQL이나 SQL Mapper 사용

OSIV

Open Session In View : 하이버네이트
Open EntityManager In View : JPA
spring.jpa.open-in-view: true (기본값)
지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고 영속성 컨텍스트는 데이터베이스 커넥션을 유지한다.
OSIV가 켜져있다면 트랜잭션 시작(최초 데이터베이스 커넥션 시작) 시점부터 API 응답이 끝날 때까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지한다.
→ 커넥션을 오래동안 유지하기 때문에 트래픽이 많을 경우 커넥션이 모자랄 수 있다.
spring.jpa.open-in-view: false
OSIV가 꺼져있다면 트랜잭션 시작(최초 데이터베이스 커넥션 시작) 시점부터 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고 데이터베이스 커넥션도 반환한다.
→ 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 즉, 트랜잭션이 끝나기 전 지연 로딩을 강제 호출해야함
커맨드와 쿼리 분리
OrderService
OrderService : 핵심 비즈니스 로직
OrderQueryService : 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)