템플릿 엔진 사용한 랜더링 컨트롤러 방식이 아닌 api 스타일의 컨트롤 방식
(@RequestBody @Valid Member memeber)
: json으로 온 body를 Member에 매핑하여 넣어준다.
@RestController //@Controller + @ResponseBody
@RequiredArgsConstructor
public class MemberApiController {
private final MemberService memberService;
@PostMapping("/api/v1/members")
// api와 엔티티를 1:1 매핑하지 말고 api 스팩을 위한 별도의 dto를 만들어 사용할 것
// 아래는 api와 Memeber엔티티가 1:1 매핑되는 방식 사용하지 말것.
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;
}
}
@PostMapping("/api/v2/members")
// api 스팩을 위한 별도의 CreateMemberRequest라는 dto생성
public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) {
Member member = new Member();
member.setName(request.name);
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberRequest {
private String name;
}
}
postman 결과
예제 코드의 v1에서 엔티티를 Request Body에 직접 매핑한 경우
문제점
결론
// MemberApiController.java
//update시에는 PutMapping를 해준다. restApi 형태
@PutMapping("/api/v2/members/{id}")
//수정 api 스팩용 dto인 UpdateMemberRequest를 생성하여 사용한다.
public UpdateMemberResponse updateMemberV2(@PathVariable("id") Long id,
@RequestBody @Valid UpdateMemberRequest request) {
// 변경 감지를 이용하기 위해 트랜잭션이 발생하여 flush시 변경 데이터가 수정되도록
// memberService.update에서 영속성 처리를 해준다.
memberService.update(id, request.name);
Member findMember = memberService.findOne(id);
return new UpdateMemberResponse(findMember.getId(), findMember.getName());
}
@Data
static class UpdateMemberRequest {
private String name;
}
@Data
@AllArgsConstructor
static class UpdateMemberResponse {
private Long id;
private String name;
}
// MemberService.java
@Transactional
public void update(Long id, String name) {
// 변경 감지를 사용하기 위해 member를 영속상태로 만들어 준다.
Member member = memberRepository.findOne(id);
member.setName(name);
}
// MemberApiController.java
// 조회 V1: 안 좋은 버전, 모든 엔티티가 노출, @JsonIgnore로 해당 필드를 안보이게 할 순 있지만
// 최악의 방법이다, api가 하나만 사용될 것이 아니기 때문.
// 엔티티를 외부에 노출해서는 안된다.
@GetMapping("/api/v1/members")
public List<Member> membersV1() {
return memberService.findMembers();
}
조회v1 결과 (배열안에 데이터가 담기기 때문에 추가적인 count등을 넣기 힘들다)
조회 V1: 응답 값으로 엔티티를 직접 외부에 노출
문제점
결론
// MemberApiController.java
@GetMapping("/api/v2/members")
public Result membersV2() {
List<Member> findMembers = memberService.findMembers();
// 엔티티를 직접 사용하는게 아니라 MemberDto를 별도로 생성해 값을 넣어준다.
List<MemberDto> collect = findMembers.stream()
.map(m -> new MemberDto(m.getId(), m.getName()))
.collect(Collectors.toList());
// 리스트로 담으면 아래와 같이 총 갯수 등을 반환할 수 있다.
return new Result(collect.size(), collect);
}
@Data
@AllArgsConstructor
static class Result<T> {
private int count;
private T data;
}
@Data
@AllArgsConstructor
static class MemberDto {
private Long id;
private String name;
}
조회v2 결과(컬렉션 안에 담기 때문에 count등 다양한 데이터를 추가할 수 있다.)
예시) 주문(Order)+배송정보(Delivery)+회원(Member)을 조회하는 API
주문(Order) 기준 연관 관계는 xToOne
Order -> Member (ManyToOne)
Order -> Delivery (OneToOne)
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
@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;
}
}
V1. 엔티티를 직접 노출하는 경우
- order -> member, order -> delivery는 지연 로딩이다. 따라서 실제 엔티티 대신에 프록시 존재
- jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모름
=> 예외 발생
- Hibernate5Module 모듈을 사용하기 위해 스프링 빈으로 등록하면 해결은 된다.
- 양방향 관계 문제 발생으로 한쪽에 반드시 @JsonIgnore를 넣어줘야 한다.
(양쪽을 계속 호출하는 무한 루프에 빠진다.)
주의
지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EAGER)으로 설정하면 안된다.
즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있고,
즉시 로딩으로 설정시 성능 튜닝이 매우 어려워 진다.
방법
=> 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용!(V3방식)
// OrderSimpleApiController.java
@GetMapping("/api/v2/simple-orders")
// List<SimpleOrderDto>를 반환하는 것 대신 Result라는 별도의 제네릭 클래스를 만들어 반환할 것
// json 결과 값을 배열이 아닌 리스트로 감쌀 수 있도록. (V3에서 사용 참고)
public List<SimpleOrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
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();
}
}
V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용X)
- 단점은 지연로딩으로 쿼리 N번 호출
- 엔티티를 DTO로 변환하는 일반적인 방법이다.
- 조회 쿼리가 총 1+N+N번 실행된다.(v1과 쿼리수 결과는 같다)
order 조회 1번(order 조회 결과 수가 N이 된다.)
order -> member 지연 로딩 조회 N번
order -> delivery 지연 로딩 조회 N번
예) order의 결과가 4개면 최악의 경우 1+4+4번 실행된다.
- 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략
// OrderSimpleApiController.java
@GetMapping("/api/v3/simple-orders")
// V2의 List<SimpleOrderDto> 반환이 아닌 Result라는 클래스로 반환
public Result ordersV3() {
List<SimpleOrderDto> result = orderRepository.findAllWithMemberDelivery().stream()
.map(o -> new SimpleOrderDto(o)) //.map(SimpleOrderDto::new) 대체 가능
.collect(Collectors.toList());
return new Result(result);
}
@Data
@AllArgsConstructor
static class Result<T> {
private T data;
}
@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();
}
}
//OrderRepository.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();
}
V3. 엔티티를 DTO로 변환 - 페치 조인 최적화
- 엔티티를 페치 조인(fetch join)을 사용해서 조회 쿼리 1번 호출
- 페치 조인으로 order -> member, order -> delivery는 이미 조회 된 상태로 지연로딩X
// OrderSimpleApiController.java
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
return orderSimpleQueryRepository.findOrderDtos();
}
// JPA에서 DTO로 바로 조회하는 용도로 패키지를 따로 분리한다.
// repository.order.simplequery 패키지에 OrderSimpleQueryDto, orderSimpleQueryRepository 추가
// OrderSimpleQueryDto.java
@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;
}
}
// orderSimpleQueryRepository.java
@Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {
private final EntityManager em;
public List<OrderSimpleQueryDto> findOrderDtos() {
// 엔티티가 아닌 dto와 매핑하는 거기 때문에 new 객체 생성으로 직접 매핑해줘야한다.
return em.createQuery("select new " +
"jpabook2.jpashop2.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();
}
}
V4. JPA에서 DTO로 바로 조회
- 쿼리 1번 호출 (V3의 페치 조인 방식과 쿼리 호출 수는 같다.)
- select 절에서 원하는 데이터만 선택해서 조회(V3의 페치 조인은 모든 필드를 조회하는 반면,
원하는 필드 예를 들어 이름, 주소만 조회 할 수 있다.)
- new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
- select 절에서 원하는 데이터를 직접 선택하므로 DB -> 애플리케이션 네트웍 용량 최적화
(생각보다 효과는 미비하다)
- 단점으로는 리포지토리 재사용성이 떨어진다, API 스펙에 맞춘 코드가 리파지토리에 들어가기 때문
쿼리 방식 선택 권장 순서
위에서도 설명 했지만, api 결과 값은 배열로 감싸는게 아니라 컬렉션으로 감싸야 한다.
코드 설명
// OrderApiController.java
// 그대로 List로 리턴하는 방식(배열로 담기게 된다) [1번]
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return result;
}
// Result로 감싸서 리턴하는 방식(컬렉션으로 담기게 된다) [2번]
@GetMapping("/api/v2/orders")
public Result ordersV2() {
/*List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());*/
List<OrderDto> result = orderRepository.findAllByString(new OrderSearch()).stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return new Result(result);
}
@Getter
static class Result<T> {
private T data;
public Result(List<OrderDto> result) {
data = (T) result;
}
}
/*
위의 Result 클래스와 동일하지만
Data 어노테이션은 많은 불필요한 것들을 포함하기 때문에
필요한 것만 선언한 위의 방식으로 사용할 것.
@Data
@AllArgsConstructor
static class Result<T> {
private T data;
}
*/
컬렉션 조회는 일대다(OneToMany)를 조회하는 방식이다.
주문내역에서 추가로 주문한 상품 정보를 추가 조회할때,
Order 기준으로 일대다 관계로 되어있는 컬렉션인 OrderItem와 Item이 필요하다.
//Order.java
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
...
// 컬렉션
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
...
}
//OrderItem.java
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
...
}
엔티티를 그대로 반환하면 안된다는 점에서 Dto를 만들어 반환할때,
껍데기만 Dto로 만든는 것이 아닌 그 안의 엔티티들도 따로 Dto로 만들어 줘야한다.
사용 코드
//OrderApiController.java
@GetMapping("/api/v2/orders")
public Result ordersV2() {
List<OrderDto> result = orderRepository.findAllByString(new OrderSearch()).stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return new Result(result);
}
@Getter
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
// OrderDto안에서도 OrderItem이라는 엔티티를 직접 쓰는게 아니라 DTO를 따로 만들어야 한다.
// 완전히 엔티티에 대한 연관 관계를 끊어야함.
//private List<OrderItem> orderItems;
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(Collectors.toList());
}
}
// OrderItem의 엔티티를 직접 사용하지 않기 위한 Dto생성
@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();
}
}
사용 코드
//OrderApiController.java
@GetMapping("/api/v3/orders")
public Result ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return new Result(result);
}
//OrderRepository.java
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엔티티의 조회 수도 증가하게 된다.
ex) 주문1번에 아이템을 2개 담았으면 결과 값은 2로 중복되는 데이터가 나오게 된다.
JPA의 distinct는 SQL문에 distinct를 추가하고, 더해서 같은 엔티티가 조회되면 애플리케이션에서
중복을 걸러준다. (SQL에서 쿼리를 입력하면 중복 데이터까지 나옴)
=> order가 컬렉션 페치 조인 때문에 중복 조회 되는 것을 막아준다.
단점: 컬렉션 페치 조인을 사용하면 페이징 처리가 불가능 하다.
위 단점의 보안으로는 아래의 V3.1을 사용하면 된다.
※ SQL과 JPA에서의 distinct 차이점
SQL에서는 쿼리에 distinct가 있으면 한 로우의 값들이 완벽하게 일치할때 중복을 제거.
JPA에서는 한 로우의 값 전체가 아니라 엔티티의 id 값이 같으면 중복을 제거 한다.
컬렉션을 페치 조인하면 페이징이 불가능하다.
해결 방안(한계 돌파)
사용 코드
//OrderApiController.java
/**
* V3.1 엔티티를 조회해서 DTO로 변환 페이징 고려
* - ToOne 관계만 우선 모두 패치 조인으로 최적화
* - 컬렉션 관계는 글로벌 프로퍼티에 hibernate.default_batch_fetch_size 또는 해당 필드에 @BatchSize로 최적화
*/
@GetMapping("/api/v3.1/orders")
public Result 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> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return new Result(result);
}
//OrderRepository.java
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();
}
ToOne인 member, delivery만 먼저 페치 조인을 건다.
ToOne인 경우에는 row수가 같기 때문에 페이징 쿼리에 문제가 없다.
쿼리 실행 결과는 우선 적으로 페치 조인한 member, delivery를 모두 불러오고
이후, 컬렉션 지연로딩으로 해당 갯수 만큼 OrderItem과 Item 각각 IN에 담겨 1번씩 호출된다.
application.yml 설정(지연 로딩 성능 최적화 설정 default_batch_fetch_size)
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/jpashop
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
# ddl-auto: none
properties:
hibernate:
# show_sql: true
format_sql: true
default_batch_fetch_size: 1000
쿼리 실행 결과
(페치 조인한 ToOne의 Member, Delivery 쿼리 1개, 컬렉션 지연로딩 OrderItem, Item쿼리 각각 1개)
V3.1방법의 장점
결론
ToOne 관계는 페치 조인해도 페이징에 영향을 주지 않는다. 따라서 ToOne 관계는 페치조인으로 쿼리 수를 줄여서 해결하고, 나머지 컬렉션은 지연로딩에 hibernate.default_batch_fetch_size 옵션을 최적화 할 것.
simple예제에서 했듯이 DTO를 따로 패키지에 선언해서 만든다.
사용 코드
//OrderApiController.java
@GetMapping("/api/v5/orders")
public Result ordersV5() {
return new Result(orderQueryRepository.findAllByDto_optimization());
}
//OrderQueryDto.java (주문DTO)
@Data
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;
}
}
//OrderItemQueryDto.java (주문에 대한 item DTO)
@Data
public class OrderItemQueryDto {
@JsonIgnore //item데이터 api에 orderId는 보여줄 필요가 없어 옵션 추가
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;
}
}
//OrderQueryRepository.java
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
/** V5
* V4의 최적화 방법
* Query: 루트 1번, 컬렉션 1번
* 데이터를 한꺼번에 처리할 때 많이 사용하는 방식
*/
public List<OrderQueryDto> findAllByDto_optimization() {
List<OrderQueryDto> result = findOrders();
//orderItem 컬렉션을 Map 한방에 조회
Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result));
//루프를 돌면서 컬렉션 추가(추가 쿼리 실행X)
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 jpabook2.jpashop2.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) {
List<Long> orderIds = result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
return orderIds;
}
/** V4
* 컬렉션은 별도로 조회
* Query: 루트 1번, 컬렉션 N번
* 단건 조회에서 많이 사용하는 방식
*/
public List<OrderQueryDto> findOrderQueryDtos() {
//루트 조회(toOne 코드를 모두 한번에 조회)
List<OrderQueryDto> result = findOrders();
//루프를 돌려서 컬렉션 추가(추가 쿼리 실행)
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
/**
* 1:N 관계(컬렉션)를 제외한 나머지를 한번에 조회
*/
public List<OrderQueryDto> findOrders() {
return em.createQuery("select new jpabook2.jpashop2.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();
}
/**
* 1:N 관계인 orderItems 조회
*/
public List<OrderItemQueryDto> findOrderItems(Long orderId) {
return em.createQuery("select new jpabook2.jpashop2.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();
}
}
엔티티 조회
- 컬렉션은 페치 조인시 페이지 불가능
- ToOne 관계는 페치 조인으로 쿼리 수 최적화
- 컬렉션은 페치 조인 대신 지연 로딩을 유지하고, hibernate.default_batch_fetch_size로 최적화
DTO 직접 조회
권장 순서
1. 엔티티 조회 방식으로 우선 접근 (최적화 변경시 수정해야 할 부분이 적다.)
1-1. 페치조인으로 쿼리 수를 최적화
1-2. 컬렉션 최적화
1-2-1. 페이징 필요시 hibernate.default_batch_fetch_size, @BatchSize로 최적화
1-2-2. 페이징 필요X -> 페치 조인 사용
2. 엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식 사용
3. DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate
※ 참 고
엔티티 조회 방식은 페치 조인이나, hibernate.default_batch_fetch_size, @BatchSize 같이 코드를 거의 수정하지 않고, 옵션만 약간 변경해서, 다양한 성능 최적화를 시도할 수 있다. 반면에 DTO를 직접 조회하는 방식은 성능을 최적화 하거나 성능 최적화 방식을 변경할 때 많은 코드를 변경해야 한다.
엔티티 조회 방식은 JPA가 많은 부분을 최적화 해주기 때문에, 단순한 코드를 유지화면서, 성능을 최적화 할 수 있다.
반면 DTO 조회 방식은 SQL을 직접 다루는 것과 유사하기 때문에, 둘 사이에 줄타기를 해야 한다.
- Open Session In View: 하이버네이트
- Open EntityManager In View: JPA
(관례상 OSIV라 한다.)
OSIV 옵션(default=true)은 간단하게 요약하면 API 응답(response)이 끝날 때 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지하는 것. (이로인해 Controller, view딴에서도 지연로딩이 가능하다.)
spring.jpa.open-in-view:true (기본값이기 때문에 true로 할 경우 생략해도 된다)
기본값으로 애플리케이션을 구동하면 시작 시점에 아래와 같이 warn로그를 남긴다.
장점
트랜잭션 시작처럼 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지. 때문에, View Template이나 API 컨트롤러에서 지연 로딩이 가능하다.
지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지하기 때문.
단점
너무 오랜시간동안 데이터베이스 커넥션 리소스를 사용하기 때문에, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다(커넥션 풀).
ex) 컨트롤러 외부에서 API 호출 -> API 응답까지 커넥션 리소스를 반환하지 못하고 유지.
application.yml설정에 spring.jpa.open-in-view:false (OSIV 종료)
장점
트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다.
따라서 커넥션 리소스를 낭비하지 않는다.
단점
모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 따라서 지금까지 위에서 작성해온
지연로딩 코드를 트랜잭션 안으로 넣어야 한다.
view template에서도 지연로딩이 동작하지 않는다.
따라서, 트랜잭션이 끝나기 전에 지연로딩을 강제로 호출해야 한다.
크고 복잡한(트래픽이 빈번한) 애플리케이션일 경우, 이 둘의 관심사를 명확하게 분리하는게 좋다.
ex) OrderService 패키지 아래
보통 서비스 계층에서 트랜잭션을 유지하기 때문에 두 서비스 모두 지연 로딩 가능
※ 참 고
고객 서비스의 실시간 API는 OSIV를 끄고, ADMIN 처럼 커넥션을 많이 사용하지 않는 곳에서는
OSIV를 켠다.
[6] QueryDSL [1/2] (0) | 2021.07.20 |
---|---|
[5] 스프링 데이터 JPA (0) | 2021.07.08 |
[2] 자바 ORM 표준 JPA 프로그래밍 - 기본편(객체지향 쿼리) [2/2] (0) | 2021.05.20 |
[1] 자바 ORM 표준 JPA 프로그래밍 - 기본편 [1/2] (0) | 2021.05.04 |
[3] JPA 활용1 강의 내용 [1/2] (0) | 2021.04.29 |