일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- JPA
- Spring Security
- HTTP
- spring boot
- virtualization
- 웹 서버
- computer science
- 자바
- 스프링 부트
- spring batch
- 스프링 시큐리티
- CS
- 가상화
- vm
- 컨테이너
- 배포
- mysql
- Spring
- 데이터베이스
- web server
- Java
- Container
- 도커
- spring cloud
- 백엔드
- 영속성 컨텍스트
- ORM
- 스프링
- 스프링 배치
- CI/CD
- Today
- Total
개발 일기
[섹션 11] 객체지향 쿼리 언어(JPQL) - 2 본문
경로 표현식
.(점) 을 통해 객체 그래프를 탐색하는 것으로
- 상태 필드(state field)
: 단순히 값을 저장하기 위한 필드 (ex: m.username)
경로 탐색의 끝으로 더이상 탐색 불가하다. - 연관 필드(association field)
: 연관관계를 위한 필드
- 단일 값 연관 필드
: @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team)
묵시적 내부 조인(inner join) 발생, 탐색O - 컬렉션 값 연관 필드
: @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)
묵시적 내부 조인 발생, 탐색X -> 결과가 컬렉션이기 때문에 속성 값에 접근할 수 없다.
- 단일 값 연관 필드
묵시적 조인은 실무(큰 프로젝트)에서는 사용을 지양해야한다. 튜닝하기 엄청 어려워진다. JOIN 쿼리가 어디서 나갔는지 확인하기 너무 어렵다. 언뜻 보면 그냥 join문 같아 보이지 않기 때문이다. 아래 쿼리문을 보면 JPQL만 보고 실제 SQL을 직관적으로 파악하기 어려움을 겪을 수 있다.
JPQL: select o.member from Order o
SQL: select m.* from Orders o inner join Member m on o.member_id = m.id
명시적 조인 vs 묵시적 조인
명시적 조인: join 키워드 직접 사용 ex)ex) select m from Member m join m.team t
묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능하다.)
ex) select m.team from Member m
// (성공) 묵시적 조인 발생(join 2개). 단일 값 연관 필드라 탐색 가능
select o.member.team from Order o
// (성공) 묵시적 조인 발생(join 1개). 컬렉션 값 연관 필드로 더 이상 탐색 불가
select t.members from Team
// (실패) 컬렉션 값 연관 필드인데 추가적인 탐색을 진행하여 옳지 않음
select t.members.username from Team t
// (성공) 상태 필드로 명시적 조인을 통해 처리하였기 때문에 옳음
select m.username from Team t join t.members m
<<실무 조언>>
조인은 SQL 튜닝에 중요 포인트인데 묵시적 조인은 조인이 일어나는 상황을 한 눈에 파악하기 어렵기 때문다. 그렇기 때문에 가급적 묵시적 조인 대신에 명시적 조인 사용. 영한님도 묵시적 조인 사용하지 않는다.
페치 조인(fetch join)
SQL 조인의 종류가 아니라 JPQL에서 성능 최적화를 위해 제공하는 기능으로 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능이다. 키워드로 join fetch를 사용하며 "[ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로" 이런식으로 사용한다.
상황을 예시로 보자
SQL 한번으로 회원을 조회하면서 연관된 티도 함께 조회하고자 한다.
// [JPQL]
select m from Member m join fetch m.team
// [SQL]
SELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
위 쿼리를 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT하지만 JPQL을 보면 m만 적고 join 뒤에 fetch만 넣어주면 된다.
엔티티 페치 조인
JPA N+1 문제
만약 위의 오른쪽 코드에서 지연로딩이라는 가정 하에 "String query = "select m from Member m"으로 했으면 어떻게 될까?
그럼 처음에 맴버 엔티티(N개)를 끌고 오면서 팀 엔티티와의 연관관계에서 지연 로딩으로 설정되어 있기 때문에 Team은 프록시 엔티티로 될 것이다. 그래서 for문에서 맴버의 이름은 그냥 쿼리가 된 상태여서 출력이 되지만 member.getTeam().name()에서 팀 엔티티에 대한 조회가 발생하면 영속성 컨텍스트를 우선 확인하고 없으면 해당 팀에 대한 쿼리가 발생한다. 그래서 회원1에서 팀A가 아직 영속성 컨텍스트에 없으니까 쿼리 한번, 회원2에서는 영속성 컨텍스트에 있을 테니 쿼리 발생하지 않고, 회원3에서는 팀B에 대해 처음 조회하는거니까 또 쿼리 한번 이렇게 발생한다. 그래서 최악의 경우에는 쿼리가 N번 발생한다.
만약 멤버가 100명이라면 처음 맴버 엔티티들을 끌고오면서 쿼리 1번, 각각에 대한 팀을 조회하면서 쿼리 100번 1+100으로 총 101번의 쿼리가 발생한다. 이러한 문제를 N+1문제라고 한다.
이러한 문제를 JPQL 페치 조인(fetch join)으로 해결할 수 있다. 위 오른쪽 코드와 같이 페치 조인(fetch join)으로 쿼리하게 되면 팀 엔티티가 프록시로 들어가는 것이 아니라 실제 inner join 쿼리를 진행해서 굳이 추후에 쿼리를 추가적으로 진행할 필요가 없다.
즉, 쿼리 N+1번 필요한 것을 1번에 처리해버린다.
컬렉션 페치 조인
일대다 관계에서의 컬렉션 페치 조인에서 우선 쿼리를 보자
-- [JPQL]
select t from Team t join fetch t.members where t.name = '팀A'
-- [SQL]
SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
이 상황은 팀A에 속한 맴버에 대해 페치 조인하는 상황이다. 이렇게 컬렉션 페치 조인의 경우 데이터 갯수가 뻥튀기가 될 수 있다.
위의 결과들을 보면 두 줄씩 나오게 된다. 각 줄의 팀A는 똑같은 팀A이 members에 똑같이 회원1, 회원2가 들어가있는 상태이다.
페치 조인과 DISTINCT
SQL의 DISTINCT는 중복된 쿼리 결과를 제거하는 하나의 역할을 하지만
JPQL의 DISTINCT 2가지 기능 제공한다. 첫번째, SQL에 DISTINCT를 추가해주고 두번째, 애플리케이션에서 엔티티 중복 제거
select distinct t from Team t join fetch t.members where t.name = ‘팀A’
SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과에서 중복제거 되지 않는다.
그것은 JPQL에서도 마찬가지 일텐데 그렇다면 두번째 기능인 애플리케이션에서 엔티티 중복 제거는 어떻게 동작할까?
위의 사진과 같이 같은 식별자를 가진 Team 엔티티를 제거한다. 그래서 결과도 team이 하나만 존재한다.
참고: 하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용됩니다.
페치 조인과 일반 조인의 차이
일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다.
// 일반 조인
-- [JPQL]
select t from Team t join [fetch] t.members m where t.name = '팀A'
-- [SQL]
SELECT T.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
// 페치 조인
-- [JPQL]
select t from Team t join fetch t.members where t.name = '팀A'
-- [SQL]
SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
위의 JPQL에서 만약 일반 join이였다면 t.getMembers()로 컬렉션(컬렉션은 프록시는 아님)을 조회하게 되면 쿼리가 또 발생하게 된다.
그러나 페치 조인을 했다면 연관된 엔티티를 같이 들고와서 쿼리 한방으로 처리해준다.
즉, 정리해보면
JPQL 자체는 결과를 반환활때 연관관계를 고려하지 않고 단지 SELECT절에서 지정한 엔티티만을 조회할 뿐이다. 그렇기 때문에 위에서 팀 엔티티만 조회하고 회원 엔티티는 조회하지 않는다.
그러나 페치 조인을 사용하게 되면 연관된 엔티티도 함께 조회(즉시 로딩)하게 되고 페치 조치 조인은 객체 그래프를 SQL 한번에 조회하는 개념이다.
페치 조인의 특징과 한계
- 페치 조인 대상에는 별칭을 줄 수 없다.
- 하이버네이트는 가능, 가급적 사용X - 둘 이상의 컬렉션은 페치 조인할 수 없다.
Team에 Members, Orders 등이 있는 경우 안된다. 일대다대다라 데이터 뻥튀기가 엄청날 수 있으며 올바르게 쿼리되지 않음. - 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
- 일대일,다대일 같은 단일값 연관필드들은 페치조인해도 페이징가능 -> because of 데이터 뻥튀기
- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
JPA 성능 최적화에 있어서 아주 중요한 부분들
- 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
- @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략 - 실무에서 글로벌 로딩 전략은 모두 지연 로딩
- 최적화가 필요한 곳은 페치 조인 적용
- 모든 것을 페치 조인으로 해결할 수는 없다.
- 페치 조인은 객체 그래프를 유지할 때 사용하며 효과적
- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면,
페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적
다형성 쿼리
TYPE
조회 대상을 특정 자식으로 한정하는 경우 ex) Item 중에 Book, Movie를 조회해라.
-- [JPQL]
select i from Item i where type(i) IN (Book, Movie)
-- [SQL]
select i from i where i.DTYPE in (‘B’, ‘M’)
TREAT
자바의 타입 캐스팅과 유사
상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
FROM, WHERE, SELECT(하이버네이트 지원) 사용
ex) 부모인 Item과 자식 Book이 있을때
-- [JPQL]
select i from Item i
where treat(i as Book).author = ‘kim’
-- [SQL]
select i.* from Item i where i.DTYPE = ‘B’ and i.author = ‘kim’
엔티티 직접 사용
JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본키 값을 사용
엔티티 직접 사용 - 기본 키 값
-- [JPQL] 이 두 개가 같은 쿼리로 변환됨
select count(m.id) from Member m -- 엔티티의 아이디를 사용
select count(m) from Member m -- 엔티티를 직접 사용
-- [SQL](JPQL 둘다 같은 다음 SQL 실행)
select count(m.id) as cnt from Member m
// 엔티티를 파라미터로 전달
String jpql = “select m from Member m where m = :member”;
List resultList = em.createQuery(jpql)
.setParameter("member", member)
.getResultList();
// 식별자를 직접 잔달
String jpql = “select m from Member m where m.id = :memberId”;
List resultList = em.createQuery(jpql)
.setParameter("memberId", memberId)
.getResultList();
/* 실행된 SQL */
select m.* from Member m where m.id=?
엔티티 직접 사용 - 외래 키 값
Team team = em.find(Team.class, 1L);
// 엔티티를 파리미터로 전달
String qlString = “select m from Member m where m.team = :team”;
List resultList = em.createQuery(qlString)
.setParameter("team", team)
.getResultList();
// 식별자를 파라미터로 전달
String qlString = “select m from Member m where m.team.id = :teamId”;
List resultList = em.createQuery(qlString)
.setParameter("teamId", teamId)
.getResultList();
/*실행된 SQL*/
select m.* from Member m where m.team_id=?
Named 쿼리
// 엔티티 정의 시점에 쿼리 정의
@Entity
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
// 실제 사용
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
- 미리 정의해서 이름을 부여해두고 사용하는 JPQL
- 정적 쿼리
- 어노테이션, XML에 정의
- 애플리케이션 로딩 시점에 초기화 후 재사용 -> 캐싱해두고 쓰기 때문에 장점.
- 애플리케이션 로딩 시점에 쿼리를 검증
-> 애플리케이션 로딩 시점에 나는 오류기 때문에 실제 버튼을 눌렀을때 나는 오류보다 나음
왼쪽과 같이 XML에다가 정의해 놓을 수도 있다.
XML이 항상 우선권을 가지고
애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.
벌크 연산
만약 재고가 10개 미만인 모든 상품의 가격을 10% 상승시킬때
JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL이 실행된다.
- 재고가 10개 미만인 상품을 리스트로 조회
- 상품 엔티티의 가격을 10% 증가
- 트랜잭션 커밋 시점에 변경감지 동작
- 만약 변경된 데이터가 100건이라면 100번의 UPDATE SQL 발생
이때 벌크 연산을 사용하게 되면??
String qlString = "update Product p " +
"set p.price = p.price * 1.1 " +
"where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(qlString)
.setParameter("stockAmount", 10)
.executeUpdate();
- 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
- executeUpdate()의 결과는 영향받은 엔티티 수 반환
- UPDATE, DELETE 지원
- INSERT(insert into .. select, 하이버네이트 지원)
주의할 점
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리해버린다.
그렇기 때문에 영속성 컨텍스트 작업 없이 벌크 연산을 먼저 실행하기. 또는 벌크 연산 수행 후 영속성 컨텍스트 초기화 해줘야한다.
'Back-End > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[섹션 10] 객체지향 쿼리 언어(JPQL) - 1 (1) | 2024.04.10 |
---|---|
[섹션 9] 값 타입 (0) | 2024.04.09 |
[섹션 8] 프록시와 연관관계 관리 (0) | 2024.04.08 |
[섹션 7] 고급 매핑 (0) | 2024.04.02 |
[섹션 6] 다양한 연관관계 매핑 (0) | 2024.04.01 |