JPA API and Performance Optimization
영한님의 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의를 요약한 내용입니다.
API Basic
API와 Template Engin은 공통 처리를 해야 하는 요소가 다르므로 패키지를 분리하여 관리하는 것이 좋음.
ㄴ src.main.java
ㄴ example
ㄴ api
ㄴ controller회원 등록 API
엔티티 대신 API 요청 스펙에 맞는 별도 DTO를 사용하기.
엔티티와 프레젠테이션(API) 계층을 위한 로직 분리할 수 있음
엔티티가 변경되어도 API 스펙이 변하지 않음
엔티티 필드가 변경되더라도 컴파일 에러로 바로 체크 가능
엔티티와 API 스펙을 명확하게 분리할 수 있음
실무에서는 절대 엔티티를 API 스펙에 노출하지 말자!
@PostMapping("/api/members")
public CreateMemberResponse saveMember(@RequestBody @Valid CreateMemberRequest request) {
Member member = new Member();
member.setName(request.getName());
member.setAddress(request.getAddress());
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberRequest {
@NotEmpty
private String name;
@Embedded
private Address address;
}
@Data
static class CreateMemberResponse {
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}회원 수정 API
등록과 마찬가지로 별도 DTO 사용하기
변경감지를 활용해서 데이터 수정하기
CQS(Command-Query Separation) : 가급적이면 Command와 Query를 분리하자.
Controller
Service
회원 조회 API
등록, 수정과 마찬가지로 엔티티를 API 응답 스펙 맞는 별도 DTO로 변환하여 반환하기
엔티티가 변경되어도 API 스펙이 변경되지 않음
Result 클래스로 컬렉션을 감싸주면서 향후 필요한 필드를 자유롭게 추가 가능
API 요청에 필요한 필드만 노출 (용도에 따라 DTO를 생성)
지연 로딩과 조회 성능 최적화
지연 로딩으로 발생하는 성능 문제를 단계적으로 해결해보자.
.
엔티티를 직접 노출
순환 참조 문제 발생
StackOverflowError@JsonIgnore 설정으로 해결 가능
@JsonIgnore 을 추가하더라도 지연로딩으로 인한 proxy(bytebuddy) 객체를 jackson 라이브러리가 읽을 수 없는 문제 발생
Type definition errorHibernate5Module 을 Spring Bean 으로 등록하면 해결 가능
단, 지연 로딩 객체는 null 출력, 강제 지연 로딩도 가능하지만 성능 악화 발생
엔티티를 DTO로 변환
엔티티를 직접 노출하지 않고,
엔티티를 DTO로 변환1 + N 문제 발생 (엔티티 직접 노출과 동일)
첫 번째 쿼리의 결과 N번 만큼 쿼리가 추가로 실행되는 문제
ex) Order 조회 시 Member - N번, Delivery - N 번, 총 1 + N + N 개의 쿼리 발생
페치 조인 최적화
페치 조인을 사용해서 1 + N 문제를 쿼리 1번 만에 조회
코드가 간결하고, 다른 API에서 재사용이 쉬움
DTO로 바로 조회
조회된 엔티티를 DTO로 변환하는 과정 필요 없이, 바로 DTO 조회해서 성능 최적화하기
원하는 필드만 선택(SELECT)해서 조회
DB <-> 네트워크 용량 최적화 (생각보다 미비한 차이)
new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
API 스펙에 맞추다보니 변경이 어려우므로, 다른 API에서 Repository 재사용이 어려움
사용할 경우 순수한 엔티티를 조회하는 레파지토리와 화면 종속적인 레파지토리를 분리하는 것을 추천
컬렉션 조회 최적화
toOne(OneToOne, ManyToOne)관계에 이어서 컬렉션인 일대다 관계(OneToMany)를 최적화해보자.
toOne 관계와 동일한 부분들이 포함되어 있다.
.
엔티티를 직접 노출
엔티티가 변하면 API 스펙이 변함
트랜잭션 안에서 지연 로딩(LAZY) 강제 초기화 필요
양방향 연관관계 문제 (@JsonIgnore 설정으로 해결 가능하지만 지연로딩 객체를 읽을 수 없는 문제 발생할 수 있음)
엔티티를 DTO로 변환
트랜잭션 안에서 지연 로딩 필요 (지연 로딩으로 너무 많은 SQL 실행)
지연 로딩은 영속성 컨텍스트에 있는 엔티티 사용을 시도하고 없으면 SQL을 실행
ex) Order 조회 시 Member - N번, Address - N 번, OrderItem - N 번, item M번
N : order 조회 수, M : orderItem 조회 수
총 1 + N + N + N + M 개의 쿼리 발생
페치 조인 최적화
페치 조인으로 SQL 1번만 실행
단, 컬렉션 페치 조인 사용 시
페이징이 불가능한 단점이 존재하이버네이트는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징 작업 - OOM 발생 위험
컬렉션 페치 조인은
1개만 사용 가능둘 이상의 컬렉션에 페치 조인을 사용하면 데이터가 부정합하게 조회될 수 있음 (1 * N * N..)
JPA
distinct는 SQL에 distinct 추가 및 같은 엔티티(=id)가 조회되면 애플리케이션에서중복을 제거1:N 조인이 있으면 데이터베이스 row가 뻥튀기되어 distinct 필요
페이징
컬렉션 페치 조인에서 페이징이 불가능한 단점 존재
일대다(1:N) 조인이 발생하므로 데이터가 일(1) 기준이 아닌 다(N)를 기준으로 row가 예측할 수 없이 증가
일(1)인 Order 기준으로 페이징 하고 싶지만, 다(N)인 OrderItem을 조인하면 OrderItem이 기준이 되어버리는 문제
이 경우 하이버네이트는 경고 로그를 남기고 모든 DB 데이터를 읽은 후 메모리에서 페이징을 시도 (최악의 경우 OOM 장애 발생)
페이징 + 컬렉션 엔티티 조회 문제 해결
ToOne(OneToOne, ManyToOne) 관계는 모두
페치 조인으로컬렉션은
지연 로딩으로 조회지연 로딩 성능 최적화를 위해
hibernate.default_batch_fetch_size(글로벌 설정) 또는@BatchSize(개별 최적화) 적용컬렉션이나, 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리 조회
default_batch_fetch_size 사이즈 선택
적당한 사이즈는 100~1000 사이 권장
IN 절 파라미터를 1000 으로 제한하는 데이터베이스가 있음(max size = 1,000)
사이즈를 높게 설정할 경우 한꺼번에 DB에서 애플리케이션으로 불러오므로 DB에 순간 부하가 증가할 수 있음.
하지만 애플리케이션은 사이즈가 어떻게 설정이 되어있든 결국 전체 데이터를 로딩해야 하므로 메모리 사용량이 같다.
1000 으로 설정하는 것이 성능상 가장 좋지만, 결국 DB든 애플리케이션이든 순간 부하를 어디까지 견딜 수 있는지 테스트를 진행해보며 결정하는 것이 중요
장점
쿼리 호출 수가 1+N 에서 1+1 로최적화IN 쿼리 사용으로 일반 조인보다
DB 데이터 전송량 최적화페치 조인 방식과 비교해서 쿼리 호출 수가 약간 증가하지만(IN 쿼리),
DB 데이터 전송량 감소(중복 제거)컬렉션 페치 조인에서
페이징이 불가능한 단점을 해결
DTO 직접 조회
ToOne(N:1, 1:1) 관계 조회한 후, ToMany(1:N) 관계는 별도 처리
ToOne 관계는 조인 시 Row 수가 증가하지 않지만, ToMany 관계는 조인 시 Row 수가 증가하여 최적화가 어려우므로 별도 조회
루트 1 번, 컬렉션 N 번, 총 1 + N 번의 쿼리 실행
코드가 단순하고, 유지보수가 쉬우며, 단건 조회에서는 유용한 방법
컬렉션 조회 최적화
IN 절을 활용해서 메모리에 미리 조회 후 최적화
루트 1 번, 컬렉션 1 번 조회
Map을 사용하여 매칭 성능 개선 - O(1)
ToOne 관계를 먼저 조회한 후, 얻은 식별자 Id로 ToMany 관계를 한꺼번에 조회
위 방법과 비교하면
발생하는 N + 1 문제를 1 + 1 로 해결
코드가 복잡해지긴 하지만, 다수의 데이터를 한 번에 조회 할 경우 환경에 따라 100배 이상 성능 최적화 가능
보통 많이 사용하는 방법
플랫 데이터 최적화
JOIN 결과를 그대로 조회한 후, 애플리케이션에서 원하는 스팩으로 직접 변환
쿼리를 한 번 실행하는 장점이 있지만,
조인으로 생기는 중복 데이터가 DB에서 애플리케이션으로 전달되어 상황에 따라 위 방법보다 느릴 수 있음
반환 Dto 스펙으로 변환을 위해 애플리케이션에서 추가 작업(변환 로직)이 필요
페이징 불가능
위 방법과 비교하면
쿼리 실행이 한 번으로 최적화가 가능하지만, 페이징이 불가능
데이터가 많아지면 중복 전송이 증가하여 위 방법과 성능 차이도 미비
조회 쿼리 권장 순서
⭐️ 조회 쿼리 방식 선택 권장 순서 ⭐️
엔티티를 DTO로 변환필요 시
페치 조인으로 성능 최적화(대부분의 성능 이슈가 해결)그래도 안되면
DTO로 직접 조회최후의 방법은
JPA가 제공하는 네이티브 SQL혹은Spring JDBC Template을 사용해서 SQL을 직접 사용
.
⭐️ 컬렉션 조회 쿼리 방식 선택 권장 순서 ⭐️
엔티티 조회 방식으로 접근페이징 필요 없을 시
페치조인으로 쿼리 수 최적화페이징 필요 시
hibernate.default_batch_fetch_size,@BatchSize로 컬렉션 최적화
엔티티 조회 방식으로 해결이 안되면
DTO 조회 방식사용DTO 조회 방식으로 해결이 안되면
NativeSQLorSpring JdbcTemplate사용
.
엔티티 조회 방식과 DTO 조회 방식
엔티티 조회 방식은 fetch join, default_batch_fetch_size, @BatchSize 등으로 코드를 거의 수정하지 않고, 옵션 설정만으로 다양한 성능 최적화 시도가 가능
반면, DTO 조회 방식은 많은 코드 변경이 필요하므로 둘 사이에 줄타기가 필요
OSIV와 성능 최적화
OSIV(
Open Session In View): hibernateJPA 에서는 Open EntityManager In View
영속성 컨텍스트가 데이터베이스 커넥션을 가지고 있는 범위
OSIV ON
spring.jpa.open-in-view: true (default)

OSIV를 사용할 경우, (View Template이나 API 컨트롤러에서의)
지연로딩을 위해 View rendering | API response 완료 시점까지 영속성 컨텍스트와데이터베이스 커넥션을 유지해야 한다.Service Layer에서 트랜젝션이 끝나더라도 rendering | API response 완료 후에야 데이터베이스 커넥션을 반환하고, 영속성 컨텍스트를 닫게 된다.
지연 로딩은 영속성 컨텍스트가 살아있어야 하므로, 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지하는 장점
하지만! 너무 오랜시간동안 데이터베이스 커넥션 리소스를 유지하므로, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 부족하여 장애로 이어질 수 있다.
컨트롤러에서 외부 API를 호출하면 외부 API 응답 대기 시간 만큼 커넥션 리소스를 반환하지 못하게 되는 단점
OSIV OFF
spring.jpa.open-in-view: false

OSIV를 끄면 트랜잭션을 종료할 때 데이터베이스 커넥션을 반환하고, 영속성 컨텍스트를 닫아 커넥션 리소스를 낭비하지 않음
단, 모든 지연로딩을 트랜잭션 안에서 처리해야 하여, 기존 지연 로딩 코드를 트랜잭션 안으로 넣거나 fetch join을 사용해야 하는 단점이 존재
view template에서 지연로딩이 동작하지 않음.
트랜잭션이 끝나기 전, 지연 로딩 강제 호출 팔요
CQS
커멘드와 쿼리 분리하여 OSIV를 끈 상태로 복잡성을 관리
보통 비즈니스 로직(등록/수정)에서는 성능이 크게 문제 없지만, 복잡한 화면을 출력하기 위한 쿼리는 성능을 최적화 하는 것이 중요
크고 복잡한 애플리케이션을 개발한다면, 이 둘의 관심사를 명확하게 분리하여 유지보수하기 쉽게 만들자.
OrderService: 핵심 비즈니스 로직
OrderQueryService: 화면이나 API에 맞춘 서비스 (주로 조회 전용 트랜잭션)
보통 서비스 계층에서 트랜잭션을 유지하므로, 두 서비스 모두 트랜잭션을 유지하면서 지연 로딩 사용 가능
추천: 고객 서비스의 실시간 API는 OSIV OFF, ADMIN 과 같이 커넥션을 많이 사용하지 않는 곳에서는 OSIV ON
소개
Spring Data JPA
Spring Data JPA는 JPA 사용 시 반복되는 코드를 자동화
org.springframework.boot:spring-boot-starter-data-jpa
JpaRepository 인터페이스에서 기본적인 CRUD 기능을 모두 제공
일반화하기 어려운 기능도 메서드 이름으로 정확한 JPQL 쿼리 실행 가능
개발자는 인터페이스만 만들면 구현체는 Spring Data JPA가 애플리케이션 실행시점에 주입
QueryDSL
QueryDSL 로 조건에 따라 실행되는 동적 쿼리를 만들 수 있다.
자바 코드로 동적 쿼리를 SQL(JPQL)과 유사하게 생성 (JPQL을 코드로 만드는 빌더 역할)
QueryDSL 장점
직관적인 문법
컴파일 시점에 빠른 문법 오류 발견
코드 자동완성
코드 재사용
JPQL new 명령어와 반대로 심플한 DTO 조회 지원
Querydsl은 JPA로 애플리케이션을 개발 할 때 선택이 아닌 필수!
Last updated