경로 표현식
간단하게 .(점)을 찍어 객체 그래프를 탐색하는 것을 의미한다.
접근하는 필드의 타입에 따라서 경로 표현식의 종류가 3가지로 나뉘게 된다.
- 상태 필드
- m.username
- 단일 값 연관 필드
- m.team t
- 컬렉션 값 연관 필드
- m.orders o
어떤 필드에 접근하는지에 따라 내부적인 JPA의 동작 방식이 달라지기 때문에, 유의해서 봐야할 개념이라고 한다.
경로 표현식에 대한 용어 정리
- 우선 상태 필드와 연관 필드로 접근할 수 있는 필드의 경로가 나뉜다.
- 상태 필드 (status field): 단순히 값을 저장하기 위한 필드
- 연관 필드 (association field): 연관관계를 위한 필드
- 단일 값 연관 필드: @ManyToOne, @OneToOne 관계에 대한 즉, 하나의 엔티티 대상
- 컬렉션 값 연관 필드: @OneToMany, @ManyToMany 관계에 대한 즉, 엔티티 컬렉션 대상
상태 필드는 경로 탐색의 끝을 의미한다. → 단순 값은 그 자체로 값을 의미하니까 당연한 말
단일 값 연관 경로와 컬렉션 값 연관 경로는 묵시적 내부 조인을 유발한다. → 연관 엔티티의 필드를 가져오기 위해서 당연
묵시적 내부 조인은 JPA가 내가 명시하지 않은 조인을 진행하는 것을 의미
다만, 컬렉션 값 연관 경로에 접근하는 경우 연관 컬렉션 이후에 대한 탐색을 금지한다.
컬렉션 값 연관 필드는 컬렉션에 대한 size만 접근 가능하다. (컬렉션의 크기를 반환하는 함수)
select t.members.username from Team t
이런게 불가능함을 의미한다. (members collection 이후에 다른 필드로의 연관 경로는 탐색이 막힌다.)
그래서 이러한 경우에는 명시적인 join을 사용해야 한다.
명시적 조인, 묵시적 조인
- 명시적 조인: join keyword를 직접 사용하는 방식을 의미
- select t from Member m join m.team t;
- 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL join이 발생 (JPA에 의해) → 내부 조인만 가능하다. (연관 필드만 접근할 수 있으니 당연한 말이다.)
- select m.team from Member m;
경로 탐색을 사용한 묵시적 조인 상황에서 주의사항
- 항상 내부 조인만
- 컬렉션은 경로 탐색의 끝이다. 탐색을 더 하고 싶은 경우 명시적 조인을 통해 별칭을 얻어야
- 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만, 묵시적 조인은 FROM 절에 영향을 준다. (join이 FROM 절에서 발생하기 때문)
실무 조언
- 가급적 묵시적 조인 대신 명시적 조인 사용을 권장!
- Join은 SQL 튜닝의 중요 요인임에 유의
- 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기에 어려움이 있다.
Fetch Join 1 - 기본
실무에서 매우 매우 매우 중요하다고 한다.
- SQL 조인의 종류가 아니다.
- JPQL에서의 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능이다!
- join fetch 명령어 사용!
- fetch join ::= [LEFT [OUTER] | INNER] JOIN FETCH 조인 경로
하나의 쿼리로 연관 엔티티 객체까지 가져오고 싶을때 사용할 수 있다. -> 즉시 로딩의 개념과 유사하지
쿼리가 3번이 나오고, 계속 정보가 들어올수록 쿼리가 많이 발생하는 문제가 생긴다.
쿼리가 복잡한 것도 문제일 수 있으나, 여러 번의 DB와의 통신 비용도 고려해야 하는 것 같다
이런 문제가 N + 1 문제이다. 즉시 로딩 / 지연 로딩 상관없이 다 발생하는 문제
→ fetch join으로 해결 가능!!
똑같은 기능을 하는데, 한 번의 쿼리로 모든 데이터를 한 번에 조회하는 방식으로 변경됨
지연 로딩 방식을 기본 방식으로 채택한 것과 상관없이 fetch join 사용 결과, 연관 엔티티 객체 정보까지 한 번에 조회되는 결과를 확인할 수 있었다.
지연 로딩 설정해도, fetch join이 우선이다!
이번에는 일대다 관계, 컬렉션에 대한 fetch join을 해보자.
DB 입장에서 컬렉션 조회할때, 데이터가 뻥튀기 되는 것을 주의해야 한다. (일대다 조인이기 때문에)
페치 조인 (fetch join)과 DISTINCT
SQL의 DISTINCT는 중복된 결과를 제거하는 명령이었다.
JPA의 DISTINCT가 제공하는 기능은
- SQL에 DISTINCT를 추가
- 애플리케이션 레벨에서 엔티티 중복을 제거
사살 이번 예제에서 어차피 sql 입장에서는 distinct 추가해도 이번 예제에서는 sql 결과가 달라지지는 않는다. (모들 컬림의 조합이 unique하도록 걸러내기 때문이다.)
하지만, 위와 같이 중복된 엔티티에 대한 제거 기능이 수행됨을 확인할 수 있었다.
조회된 결과에서 같은 식별자를 갖는 Team 엔티티를 제거한다.
Fetch join과 일반 join의 차이
일반 JOIN에서는 연관된 엔티티를 함께 조회하지 않는다.
이렇게 일반 조인을 하는 경우 프로젝션 과정에서 엔티티를 직접 다 가져오도록 JPQL을 작성해줘야 하고, 조회 과정도 훨씬 까다롭다. (더 잘 조회할 수 있겠지만, 일단 혼자 저 단계까지만 실습을 진행해봤다.)
- JPQL은 결과를 반환할때, 연관관계를 고려하지 않는다.
- 단지 SELECT 절에 지정한 엔티티만을 조회할뿐
- 위의 예시 상황에서는 팀 엔티티만을 조회하고, 회원 엔티티는 조회하지 않는 결과를 확인할 수 있었다. (직접 명시하지 않는 이상)
하지만, 페치 조인을 사용하는 경우
- 연관된 엔티티도 함께 조회(즉시 로딩)
- 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념과 같이 동작한다.
Fetch Join 2 - 한계
- 페치 조인 대상에는 별칭을 부여할 수 없다! (JPA 표준)
- 하지만, hibernate 사용하는 경우에는 가능 (가급적 사용하지 말자)
- 기본적으로 나와 연관된 모든 객체를 가져오는 방식이기 때문!! (별칭을 쓰는건 따로 객체 그래프를 탐색할 수 있음을 의미)
- 연관된 일부만 조회해서 따로 조작한다?? → 매우 위험한 행위 (걸러서 조회하는게 좋아 보이지만, 그렇지 않다고 한다)
- 둘 이상의 컬렉션은 페치 조인 불가능
- 일대다 한 개 만으로 뻥튀기 되는 데이터를 두 번 반복한다면?? → 엄청난 뻥튀기가 발생할 것이다.
- 컬렉션을 페치 조인하면, 페이징 API (setFirstResult, setMaxResults)을 사용할 수 없다.
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다.
- 방향을 뒤집어서 해결하자.
- 컬렉션을 페치 조인하고, 페이징을 적용하면, 연관된 일부 객체만을 조회하는 문제로 이어지기 때문!!
- hibernate는 경고 로그를 남기고 메모리에서 페이징을 한다고 함. (매우 위험하다)
- 모든 데이터를 가져온 다음에 메모리에서 직접 처리해주는 방식이기 때문에, 매우 위험
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다.
N + 1 문제의 해결을 fetch join이 아닌 batch size를 부여해서도 가능하다.
@BatchSize(size = 원하는 수 <= 1000) 어노테이션을 활용한 설정 부여 방식으로 동작하도록 구현이 가능. 혹은 global setting으로 설정할 수도 있다.
1번 쿼리의 결과로 조회된 N개의 데이터에 대한 N번의 조회로 인해 발생된 N + 1 문제가
N번의 조회를 1번으로 줄임으로써 2번의 조회로 줄이는데 성공할 수 있다. (물론 이 설정상 N이 100보다 작은 상황에서)
페치 조인 - 정리
- 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한다.
- @OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
- 실무에서 글로벌 로딩 전략은 모두 지연 로딩으로 설정하고
- 최적화가 필요한 곳에 fetch join을 적용해서 성능을 최적화한다고 함
하지만
- 모든 것을 fetch join으로 해결할 수는 없다.
- fetch join은 객체 그래프를 유지할때, 사용하면 효과적인 방법
- 여러 테이블을 조회해 엔티티의 모양이 아닌 전혀 다른 결과가 필요한 경우에는, fetch join 보다 일반 join을 사용한 후 필요한 데이터만을 조회해서 DTO로 반환하는 것이 효과적이다.
JPQL - 다형성 쿼리
사실 크게 중요한 부분은 아니라고 한다.
예를 들어 위와 같이 다형적으로 설계한 경우
- 조회 대상을 특정 자식 엔티티 타입으로 제한하기 위한 TYPE
- 자바의 타입 캐스팅과 유사하게 사용 가능한 TREAT가 있다.
TYPE
조회 대상을 특정 자식 엔티티 타입으로 한정하기 위해 사용하며
- Item 중 Book, Movie를 조회하고 싶은 경우
- [JPQL]
select i from Item t where type(i) in (Book, Movie); - [SQL]
select i.* from item where i.DTYPE in ('B', 'M');
이런 식으로 사용할 수 있다.
TREAT
자바의 타입 캐스팅과 유사하게 사용 가능하며, 부모 타입을 특정 자식 타입으로 다루고 싶을때 사용할 수 있다.
FROM, WHERE, SELECT(hibernate) 에서 사용 가능
- 부모인 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';
이런식으로 SQL로 매핑해준다.
discriminator column 역할을 한 DTYPE column에 조건절을 알아서 걸어주는 기능을 제공한다고 이해할 수 있었다.
엔티티를 직접 사용하는 경우
엔티티를 기본 키 값으로 대체
sql은 엔티티 전체를 사용하는 경우가 없다. 하지만, JPQL은 다음과 같이 엔티티를 직접 사용하는 경우들이 있는데, 이때 엔티티 대신 엔티티의 기본 키 값을 사용한다.
select m.* from Member m where m.id = ?;
외래 키로 대체
또는 다음과 같이 엔티티를 JPQL에서 직접 다룰때, FK로 매핑되는 경우가 있다.
Member entity의 Team 엔티티 필드로 접근해서 비교를 하든, 식별자를 사용하든 관계없이 foreign key를 이용한 조회 쿼리로 해석되는 결과를 확인할 수 있었다.
요약하자면, JPQL에서 엔티티를 직접 비교의 대상으로 사용할 수 있는데, 이는 JPA에 의해 적절하게 알맞은 PK 혹은 FK로 대체된다는 사실을 알 수 있었다.
Named 쿼리
- 미리 정의해서 이름을 부여해, 재사용할 수 있는 JPQL을 의미
- 정적 쿼리만 가능하다.
- 어노테이션 or XML에 정의
- 애플리케이션 로딩 시점에 초기화 후 재사용된다. (로딩 시점에 캐싱됨)
- 애플리케이션 로딩 시점에 쿼리를 검증 (가장 좋은 에러는 컴파일 에러!!)
이렇게 정의하고
createNamedQuery method를 이용해 사용할 수 있다.
또한 XML에 정의해서 사용할 수도 있다.
- xml 설정이 항상 우선권을 갖고
- 애플리케이션 운영 환경에 따라 다른 XML을 배포할 수도 있다.
Spring Data JPA에서는 인터페이스 메서드에 named query를 등록하는 기능을 제공한다고 함.
-> 이걸 이름 없는 named query라고 한다고..
엔티티에 네임드 쿼리를 넣는 것은 지저분하고, 그렇게 권장되는 방식은 아니라고, 왜냐하면 실무에서는 Spring Data JPA를 사용하는 것이 주류이기 때문이다.
JPQL - 벌크 연산
일반적인 SQL의 UPDATE, DELETE 문이라고 보면 된다고 한다. (하나를 찝어서 값을 수정하거나 삭제하는 것을 제외한 UPDATE나 DELETE)
- 예를 들어 재고가 10개 미만인 모든 상품의 가격을 10% 상승시키려면?
- JPA 변경 감지 기능을 활용하기에는 너무 많은 SQL이 실행되어야 한다.
- 재고가 10개 미만인 상품 리스트를 조회
- 상품 엔티티의 가격을 10% 증가 시킴
- 트랜잭션 커밋 시점에 변경 감지 작동
- 변경되는 데이터 100개 라면? → 100번의 UPDATE query
예제로 확인해 보자.
- 쿼리 한 번으로 여러 테이블의 로우를 변경한다. (엔티티를 변경)
- executeUpdate() method 결과는 영향받은 엔티티의 수를 반환한다.
- UPDATE, DELETE 문을 지원
- INSERT (insert into .. select, hibernate가 지원한다)
벌크 연산 주의할 점
- 벌크 연산은 영속성 컨텍스트를 건너띄고, DB에 직접 쿼리
- 따라서
- 벌크 연산 작업을 가장 먼저 실행하거나
- 벌크 연산 이후에는 필수적으로 영속성 컨텍스트를 초기화 (with clear method) 해야 한다.
기존의 영속성 컨텍스트에서 관리되던 객체들은 준영속 상태가 되기 때문에, 영속성 컨텍스트를 초기화 한 이후에 다시 조회해서 사용해야 한다!
'BackEnd > java spring' 카테고리의 다른 글
[Spring MVC] HttpServletRequest, HttpServletResponse (0) | 2023.03.06 |
---|---|
Servlet에 대한 개념 정리 (0) | 2023.03.01 |
[JPA] JPQL 1 - 기본 문법 정리 (0) | 2023.02.22 |
[JPA] 값 타입 (1) | 2023.02.18 |
[JPA] 프록시 (0) | 2023.02.15 |
댓글