[JPA2] API 개발 고급 실무편 - OSIV와 성능 최적화 (+ 스프링데이터JPA)

레몬커드요거트·2022년 11월 10일
0

OSIV

Open Session IN View : 하이버네이트
향후에 JPA 생성되면서, Open EntitiyManager In View로 바뀌면서 관례상 OSIV로 불리움.

WARN 6208 --- [           main]
JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open
in-view is enabled by default. Therefore, database queries
may be performed during view rendering. Explicitly configure
spring.jpa.open-in-view to disable this warning

OSIV ON

spring.jpa.open in-view: TRUE 기본값
OSIV전략은 데이터베이서 커넥션 시작 시점부터 API 응답이 끝날 때까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지함.

장점: View Template이나 API 컨트롤러에서 지연 로딩이 가능.

단점: 너무 오랜시간 데이터베이스 커넥션 가지고있으면 실시간 트래픽이 중요한 어플리케이션에선 문제가 될 수 있음 -> 커넥션이 말라버림

OSIV OFF

spring.jpa.open in-view: FALSE
OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환

장점:

  • 커넥션 리소스를 낭비하지 않음

단점:

  • OSIV를 끄면 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 따라서 지금까지 작성한 많은 지연 로딩코드를 트랜잭션 안으로 넣어야함.
  • view template에서 지연로딩이 동작하지않음. 결론적으로 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해야함.

커맨드와 쿼리 분리

OSIV를 끈 상태로 복잡성을 관리하는 방법: Command 와 Query 분리

OrderService //서비스 계층에서 트랜잭션 유지

  • OrderService: 핵심 비즈니스 로직
    • 비즈니스 로직은 특정 엔티티 몇 개를 등록하거나 수정하는 것이므로 성능이 크게 문제가 되지 않음
  • OrderQueryService: 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)
    • 복잡한 화면을 출력하기 위한 쿼리는 화면에 맞추어 성능을 최적화 하는 것이 중요

고객 서비스의 실시간 API는 OSIV를 끄고, ADMIN 처럼 커넥션을 많이 사용하지 않는 곳에서는 OSIV를 켠다.

스프링 데이터 JPA

https://spring.io/projects/spring-data-jpa

package jpabook.jpashop.repository;

import jpabook.jpashop.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import javax.persistence.PersistenceContext;
import javax.persistence.EntityManager;
import java.util.List;

@Repository //자동으로 spring bean으로 repository 등록, component scan의 대상
@RequiredArgsConstructor
public class MemberRepository {

    /*
    @PersistenceContext
    private EntityManager em;*/
    private final EntityManager em;

    public void save(Member member){ //회원 저장
        em.persist(member);
    }

    public Member findOne(Long id){ //회원 단건 조회
        return em.find(Member.class, id);
    }

    public List<Member> findAll(){ //회원 리스트 조회
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
    public List<Member> findByName(String name){ //회원을 조회하는데 이름에 의해서 조회
        return em.createQuery("select m from Member m where m.name = :name", Member.class ) //JPQL에 의해
                .setParameter("name",name)
                .getResultList();
    }
}
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface MemberRepository extends JpaRepository<Member, Long> {
	List<Member> findByName(String name);
}

findByName : select m from Member m where m.name =? 자동 생성


  • 스프링 데이터 JPA는 JpaRepository 라는 인터페이스를 제공하는데, 여기에 기본적인 CRUD 기능이 모두 제공된다.
  • findByName 처럼 일반화 하기 어려운 기능도 메서드 이름으로 정확한 JPQL 쿼리를 실행한다.
    • select m from Member m where [m.name](http://m.name/) = :name
  • 개발자는 인터페이스만 만들면 된다. 구현체는 스프링 데이터 JPA가 애플리케이션 실행시점에 주입해준다

QueryDSL

http://www.querydsl.com/

실무에서는 조건에서 실행되는 쿼리가 달라지는 동적쿼리 많이 사용

JPQL을 자바코드로 작성할 수 있게 해줌.
JPA Criteria 사용이 가능하지만, 유지보수가 어려움.

 /**
   * JPA Criteria
   */

public List<Order> findAllByCriteria(OrderSearch orderSearch) {
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery<Order> cq = cb.createQuery(Order.class);
  Root<Order> o = cq.from(Order.class);
  Join<Object, Object> m = o.join("member", JoinType.INNER);

  List<Predicate> criteria = new ArrayList<>();

  //주문 상태 검색
  if (orderSearch.getOrderStatus() != null) {
    Predicate status = cb.equal(o.get("status"), orderSearch.getOrderStatus());
    criteria.add(status);
  }
  //회원 이름 검색
  if (StringUtils.hasText(orderSearch.getMemberName())) {
    Predicate name =
      cb.like(m.<String>get("name"), "%" + orderSearch.getMemberName() + "%");
    criteria.add(name);
  }

QueryDSL이용방법

buildscript {
    ext {
        queryDslVersion = "5.0.0"
    }
}
...
plugins {
    id 'org.springframework.boot' version '2.6.4'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    // querydsl 플러그인 추가
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
...
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    implementation 'mysql:mysql-connector-java'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
    // querydsl 디펜던시 추가
    implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
    implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
}
...
// querydsl 사용할 경로 지정합니다. 현재 지정한 부분은 .gitignore에 포함되므로 git에 올라가지 않습니다. 
def querydslDir = "$buildDir/generated/'querydsl'"
 
// JPA 사용여부 및 사용 경로 설정
querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
 
// build시 사용할 sourceSet 추가 설정
sourceSets {
    main.java.srcDir querydslDir
}
 
 
// querydsl 컴파일 시 사용할 옵션 설정
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}
 
// querydsl이 compileClassPath를 상속하도록 설정
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    querydsl.extendsFrom compileClasspath
}

gradle 7.x버전 이용 시 -> build.gradle 작성시, 오류 해결!

OrderRepository

public List<Order> findAll(OrderSearch orderSearch){
        QOrder order = QOrder.order;
        QMember member = QMember.member;

        //이전이랑 비교안되게 JPA랑 비슷하게 직관적임!
        //컨디션도 넣을 수 있음
        //컴파일 시점에 오타가 잡힌다는 큰 장점도 있음!!
        JPAQueryFactory query = new JPAQueryFactory(em);
        return query
                .select(order)
                .from(order)
                .join(order.member, member)
                .where(statusEQ(orderSearch.getOrderStatus()), member.name.like(orderSearch.getMemberName()))
                .limit(1000)
                .fetch();
    }

    //멤버네임 동적쿼리로 짜기
    private BooleanExpression nameLike(String nameCond) {
        if (!StringUtils.hasText(nameCond)) {
            return null;
        }
        return QMember.member.name.like(nameCond);
    }

    //status 동적쿼리로 짜기
    private BooleanExpression statusEQ(OrderStatus statusCond){
        //상태가 널이면 안쓰고 버림
        if (statusCond == null){
            return null;
        }
        //아니면 스테이터스 제대로 반환
        return QOrder.order.status.eq(statusCond);
    }

실무에서는 복잡한 동적 쿼리를 많이 사용하게 되는데, 이때 Querydsl을 사용하면 높은 개발 생산성을 얻으면서 동시에 쿼리 오류를 컴파일 시점에 빠르게 잡을 수 있다.

  • 직관적인 문법
  • 컴파일 시점에 빠른 문법 오류 발견
  • 코드 자동완성
  • 코드 재사용(이것은 자바다)
  • JPQL new 명령어와는 비교가 안될 정도로 깔끔한 DTO 조회를 지원한다

Querydsl은 JPA로 애플리케이션을 개발 할 때 선택이 아닌 필수라 생각한다.

profile
비요뜨 최고~

0개의 댓글