반응형
API 개발
- @Valid + (@NotEmpty와 같이 사용하여 유효성체크)
//스프링 부트 2.3.0 이상 버전에서는
implementation 'org.springframework.boot:spring-boot-starter-validation'
- @RequestParam : 요청 파라미터를 1:1로 받아옴. @RequestParam을 붙여주면 필수적으로 값을 넘겨줘야한다.(required 값을 false로 바꿔주면 넘겨주지않아도 됨), 요청 파라미터랑 메서드 파라미터 이름이 같은 경우 생략 가능
- @RequestBody : http 요청 body를 객체로 변환시켜준다. body가 존재하지 않는 get방식에는 사용불가하며 post방식과 함께 사용된다. json이나 xml 형태의 데이터를 message converter를 통해 객체로 바인딩한다.
- @JsonIgnore : getter/setter json 변환시 해당 필드를 무시
- @ResponseBody : 메소드에서 리턴되는 값이 view를 반환하지않고, 자바 객체를 message converter를 통해 http response body 에 쓰여진다.
- @RestController = @Controller + @ResponseBody
- 예외핸들러
@ExceptionHandler({CustomException.class, HelloException.class}) // 클래스 안에서 예외처리 핸들러
public ResponseEntity<String> handle(RuntimeException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
// 클래스에 @ControllerAdvice @RestControllerAdvice 있다면 전역에서 예외처리 가능
- DTO와 Entity 분리
- 엔티티에선 JPA 어노테이션만, 뷰 validation은 API스팩을 위한 별도 DTO새로 생성
엔티티 스펙이 변하면 API 스펙도 변하기 때문 - 엔티티를 파라미터로 받지말기/ 리턴으로 반환않기(노출X) -> 사이드 이펙트 줄이기
엔티티에 @JsonIgnore, @Valid 추가하지 않기
@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); }
- 엔티티에선 JPA 어노테이션만, 뷰 validation은 API스팩을 위한 별도 DTO새로 생성
- 응답값을 리스트로 바로 반환시 확장성 떨어지므로, 껍대기가 필요하다.
@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); } @Data @AllArgsConstructor static class Result<T> { private T data; //새로운 컬럼 확장가능 } @Data @AllArgsConstructor static class MemberDto { private String name; }
지연 로딩과 조회 성능 최적화
- 오류 해결
- 양방향인 경우 한쪽에 @JsonIgnore 붙여줘야 한다. (조회시 무한루프 되기 때문)
- 지연로딩으로 인해 Proxy객체를 초기화한 상태라 jackson 라이브러리가 형변환 실패
(DTO사용해야 하는것을 추천하므로 고려하지 않아도 되긴함)
-> Hibernate5Module 등록으로 강제 지연로딩 가능 (혹은 .getName()과 같이 함수 호출로 지연로딩실행)
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
@Bean Hibernate5Module hibernate5Module() { return new Hibernate5Module(); }
- ORDER -> SQL 결과 row 2개 -> 각 row마다 테이블 1개씩 더 조회 -> 총 5번의 쿼리
@GetMapping("/api/v2/simple-orders") public List<SimpleOrderDto> orderV2() { //N + 1 -> 1 + 회원 N(2) + 배송 N(2) -> 총 5개 return orderRepository.findAll(new OrderSearch()) .stream().map(o -> new SimpleOrderDto(o)) .collect(Collectors.toList()); }
- 패치조인 사용
//코드 간편, 재사용 가능 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(); } //애플리케이션 네트웍 용량 최적화(생각보다미비) //재사용성 희미 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(); }
- 권장 순서
- 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
- 필요하면 페치 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결된다.
- 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
- 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접
컬렉션 조회 최적화 (버전별 조회)
엔티티조회
- 엔티티노출
- 엔티티가 변하면 API 스펙이 변한다
- 트랜잭션 안에서 지연 로딩 필요
- 양방향 연관관계 JsonIgnore
- Dto 생성
- Dto에도 entity가 포함돼있으면 안된다. (연관관계 모두 끊기, Dto가 Dto가지도록)
- 트랜잭션 안에서 지연 로딩 필요
- 지연로딩으로 인한 N + 1 문제
- fetch join
- SQL 1번실행 ( 네트워크 데이터 전송량 커짐 )
- distinct로 엔티티 중복을 어플리케이션에서 중복제거 가능
- 컬렉션 fetch조인은 1개만 사용하도록 하자(데이터를 못맞출 수도 있다)
- 페이징불가 **
**(전체 결과를 어플리케이션에 올려서 인메모리 페이징처리, 원하는 페이징 결과의 값도 다를수도 있으니까)
public List<Order> findAllWithItems() {
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();
}
페이징과 한계 돌파 ( 1 + N -> 1 + 1)
- ToOne 관계를 모두 패치조인 ( row 증가 안하므로 )
- ToMany는 지연 로딩으로
- hibernate.default_batch_fetch_size: 100 사용시 모든 지연로딩에서 IN 쿼리로 100개씩 날림
- 100 ~ 1000 사이로 설정
- 네트워크 데이터 정규화 되어 전송됨
- @BatchSize(size = 100)은 개별 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();
}
DTO조회
- Dto 직접조회
- View용 Dto와 Query용 Dto의 분리 고려
- ToOne 관계를 먼저 조회
- (그냥 조인, 쿼리 한번)
- ToMany 관계는 각각 별도로 처리
- (역으로 ToOne쪽에서 그냥 조인해서 가져와야, 첫번째 결과만큼 N번쿼리 발생)
- N + 1 문제
public List<OrderQueryDto> findOrderQueryDtos() {
//루트 조회(toOne 코드를 모두 한번에 조회)
List<OrderQueryDto> result = findOrders();
//루프를 돌면서 컬렉션 추가(추가 쿼리 실행)
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
private 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();
}
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();
}
- Dto 컬렉션조회 최적화
- ToOne 관계를 먼저 조회
- (그냥 조인, 쿼리 한번)
- 여기서 얻은 식별자 orderId로 In절로 조회
- 쿼리 1번 조회
- Map에 결과를 넣고 컬렉션에 add해주기(분해조립)
- SQL 사용하지 않고 메모리에서 작업
- ToOne 관계를 먼저 조회
public List<OrderQueryDto> findAllByDto_optimizer() {
List<OrderQueryDto> result = findOrders();
List<Long> orderIds = result.stream().map(o -> o.getOrderId()).collect(Collectors.toList());
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()));
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
- 플랫 데이터 최적화
- Dto가 Dto 갖도록해서 1번에 조회
- 쿼리 1번
- DB값으로 중복데이터가 되므로 더 느릴 수 있다.
- 어플리케이션 추가 작업이 복잡하다. (분해 조립)
- 페이징 불가능
- Dto가 Dto 갖도록해서 1번에 조회
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ooderV6() {
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 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();
}
적용 순서
- 엔티티 조회 방식 우선 (코드변경이 적고, 생산성이 높다)
- 페치조인으로 쿼리 수를 최적화
- 컬렉션 최적화
- 페이징 필요 hibernate.default_batch_fetch_size , @BatchSize 로 최적화
- 페이징 필요X 페치 조인 사용
- 엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식 사용
- V4는 코드가 단순, 조회한 Order 데이터가 1건이면 OrderItem을 찾기 위한 쿼리도 1번만 실행
- V5는 코드가 복잡, 여러 주문을 한꺼번에 조회하는 경우에는 V4 대신사용
- V6는 쿼리는 1번이지만, 페이징이 불가능하고 데이터가 많으므로 성능 차이가 미비할 수 있다.
- DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate
OSIV와 성능 최적화
- Open Session In View: 하이버네이트
Open EntityManager In View: JPA
- OSIV ON
- spring.jpa.open-in-view : true 기본값
- 언제 DB Connection을 가져오고 반환하나의 문제
- OSIV 전략은 트랜잭션 시작처럼 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지한다. 그래서 지금까지 View Template이나 API 컨트롤러에서 지연 로딩이 가능했던 것이다.
- 지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지한다. 이것 자체가 큰 장점이다. ( return OR view rendering 돼야 반환)
- 그런데 이 전략은 너무 오랜시간동안 데이터베이스 커넥션 리소스를 사용하기 때문에, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다. 이것은 결국 장애로 이어진다. 예를 들어서 컨트롤러에서 외부 API를 호출하면 외부 API 대기 시간 만큼 커넥션 리소스를 반환하지 못하고, 유지해야 한다.
- OSIV OFF
- spring.jpa.open-in-view: false
- OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소스를 낭비하지 않는다. OSIV를 끄면 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 따라서 지금까지 작성한 많은 지연 로딩 코드를 트랜잭션 안으로 넣어야 하는 단점이 있다.
- 그리고 view template에서 지연로딩이 동작하지 않는다. 결론적으로 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해 두어야 한다.
- 해결법
- 커맨드와 쿼리 분리
- OrderService : 핵심 비즈니스 로직
- OrderQueryService : 화면이나 API에 맞춘 서비스 ( 주로 읽기 전용 트랜잭션 사용), 프록시 초기화용
- 커맨드와 쿼리 분리
- 실시간 : OFF
- ADMIN : ON
728x90
반응형
'스프링 > JPA' 카테고리의 다른 글
JPA 상속 관계 (TABLE_PER_CLASS전략) (0) | 2021.05.21 |
---|---|
JPA 활용 1 (0) | 2021.04.23 |
자바 ORM 표준 JPA (0) | 2021.04.16 |
스프링-입문-스프링부트 + JPA (0) | 2021.04.16 |
다중 DBconnection (with JPA) (0) | 2021.02.27 |
댓글