경로표현식

.(점)을 찍어 그래프를 탐색하는 것을 말한다.

  • m.username 상태필드 /// 단순히 값 저장
  • m.team 단일 값 연관필드 // OneToOne, ManyToOne
  • m.orders 컬렉션 연관필드 // OneToMany, ManyToMany

이렇듯, 세가지 경로 표현식있고
어떤 필드를 사용하냐에 따라 내부동작이 달라지므로 꼭 구분해서 사용하자.

<br>

상태필드는 경로 탐색의 끝이다, 탐색을 더이상하지 않는다.

단일 값 연관 경로는 묵시적으로 JOIN 쿼리가 발생하고, 더 깊은 탐색이 가능하다. ex) m.team.teamName

컬렉션 값 연관 경로는 묵시적으로 JOIN 쿼리가 발생하지만, 더 깊은 탐색은 불가능하다. 명시적 조인을 통해 별칭을 얻어야 더 깊은 탐색이 가능하다.
ex) select t.members.username from Team t > 불가
ex) select m.username from Team t join t.members m > 가능

중요한 것은 가급적 묵시적 조인이 아닌, 명시적 조인을 사용해야 한다는 것이다.
앞서 언급햇듯이, JOIN 성능상의 이슈이고, 튜닝해야 하는 문제중에 하나이다. 의도치않게 JOIN이 나가는 경우는 항상 주의해야한다.

<br>

페치 조인

페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념임을 기억하자.

예를들어, 회원을 조회하면서 팀을 항상 JOIN해서 쓴다고 생각하자.

select m from Member m join m.team을한다면,

Member를 가져오고, 각 MemberTeamSELECT하는 쿼리가 호출된다.
따라서 MemberN개라면 N+1의 쿼리가 호출된다.

페치 조인은 이를 해결해준다.

select m from Member m join fetch m.team로 조회한다면,
쿼리 호출시, JOIN을 해서 조회한다. 따라서 1번의 쿼리의 요청으로 데이터를 조회 할 수 있다.

<br>

컬렉션 또한 페치 조인 가능 하다.

select t from Team t join fetch t.members로 조회한다면,
JOIN을 해서 조회한다.

하지만 주의 해야 할 점이 있다.
JOIN1:N, N:M관계에서는 중복되는 데이터가 발생하는 것이다.

JPQL의 DISTINCT 키워드는 이 문제를 해결해준다.
SQL의 DISTINCT의 중복된 결과를 제거하는 것에 더해서,
애플리케이션 단에서 ENTITY의 중복 또한 제거해주는 것이다.

select distinct t from Team t join fetch t.members

<br>

페치 조인의 한계

첫번째로, 페치조인에 대상에는 별칭을 줄 수 없다.

기본적으로 JPA 설계 사상 자체가, 연관된 값을 다가져오는 것을 기본으로하는 것을 명심해야한다.

따라서, 페치조인 이라는 것은 연관된 것을 다 가져와야하는 것이다.

다음 코드는 불가능하다, (가능하긴 하지만 쓰면안된다.)
select t from Team t join fetch t.members m
select t from Team t join fetch t.members m where m.age > 15

예를들어 A TeamMember가 10명이 있다.
별칭을 주고 Member에 조건을 주어, A Team에 소속된 3명의 Member만 가져온다.
그리고, 이번엔 조건을 주지않고, 10개의 Member를 모두 가져온다.

똑같은 A Entity 임에도 불구하고, 데이터의 불일치가 발생하는 것이다.
이는 곧 문제점으로 이어질 것이다.

그럼에도 조건을 주고 3명의 Member만 가져오고싶다면,
3명을 조회하는 Member를 따로 쿼리를 요청하도록하자.

<br>

두번째로, 둘 이상의 컬렉션은 페치 조인을 하면 안된다.

select t From Team t join fetch t.members m join fetch t.orders

1:N도 중복된 곱하기가 N만큼 생기는데,
거기에 또 1:N을 한다면, 또 곱하기 N을 하는 것이다.
그렇다면 많은 중복된 데이터가 발생하고, 지나친 성능 문제로 이어진다.
또, 잘못된 데이터가 나올 가능성도 있다.

<br>

세번째로, 컬렉션을 페치 조인하면 페이징 API를 사용 할 수 없다.

예를 들어 1:N관계에서, 페이지 사이즈를 10으로 주자.
하지만 컬렉션 페치 조인을 하게 되면, 검색된 데이터의 row가 곱하기가 되버린다.
row가 적다면 괜찮을 수도 있지만, 대량의 데이터라면,
어플리케이션에서 Entity DITINCT를 하는데, 많은 리소스를 필요로 할 것이다.

따라서, 사용할 수 있지만, 사용한다면 WARNING 메세지를 확인 할 수 있다.
컬렉션 페이조인은 페이징 API에서 사용되지 않음이 권장되고, 또 그렇게 해야한다.

결국 페이징에서는 페치 조인을 사용할 수 없고,
LAZY하게 사용할 수 있으며, 따라서 N+1의 문제가 발생한다.

이 문제는, 단순히 다음과 같이 해결 할 수도 있다.
1:NN:1로 뒤집는 것이다.

select Member From m t join fetch m.team

하지만, 이 문제를 해결 할 수 있는 좀더 명확한 방법은
BATCH SIZE라는 개념을 사용하는 것이다.
default_batch_fetch_size를 사용하여 기본 사이즈를 지정하거나
@BatchSize를 사용하여 정의 할 수 있다.

예를들어 Team을 10개를 조회한다면,
각 팀의 Member를 조회하는 10개의 쿼리를 더 요청하게 된다.

하지만 BATCH SIZE를 설정한다면 10개의 쿼리가 아닌,
한번의 쿼리로 요청을 해결할 수 있다.

select m from Member m where m.team.id in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

BATCH_SIZE만큼 Member를 한 번에 조회하여,
메모리에 저장하여 가지고 있는 것이다.
TeamMember를 조회할 때, 쿼리 요청이 아닌, 메모리에 저장된 값을 반환한다.

정리하면 다음과 같다.

  • 모든 것을 페치 조인으로 해결 할 수가 없다.
  • 페치 조인은 객체 그래프를 유지할 때 사용 하면 효과적이다.
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야한다면, 페치 조인보다는 일반 조인을 사용하고 필요한 데이터만을 조회해서 DTO로 반환하는것이 효과적일 것이다.

<br>

다형성쿼리

다형성 쿼리는 크게 중요한 부분은아니다.
entity를 상속관계로 정의 했을 경우 사용한다.

TYPE
조회 대상을 특정 자식으로 다음과 같이 한정 할 수 있다

select i from item i
where type(i) IN (Book, Movie)

<br>

TREAT
자바의 하위 캐스팅과 비슷한 개념이다

selct i from Item i
where treat(i as Book).author = 'kim'

<br>

엔티티의 직접 사용

JPQL에서 엔티티를 직접 사용하면 SQL에서 어떻게 반영되는지를 보자.

select count(m.id) from Member m
select count(m) from Member m // 엔티티 직접사용

<br>

JPA는 JPQL을 번역하면서, 엔티티를 엔티티의 id를 사용하여 번역 한다.

select count(m.id) from Member m

<br>

파라미터로 넘겨도 마찬가지로 id를 사용한다.

String jpql "slect m from Member m where m = :member"; 
em.createQuery(jpql)
    .setParamemeter("member", member) //엔티티 직접 사용
    .getResultList();
select m.* from Member m where m.id = ?

<br>

Named Query

Named Query는 JQPL Query를 미리 정의하고,
재사용 할 수 있도록 하는 것이다.

어노테이션, XML 방식(잘쓰지않음) 으로 정의 할 수 있다.
(우선순위는 항상 XML 방식이 높다)

중요한것은 Named Query는 정적 쿼리라는 것이다.

이 말은 즉, 애플리케이션 로징 시점에 초기화가 된다는 것 이고,
다시 번역하면, 어플리케이션 로딩 시점에 쿼리를 검증 할 수가 있다.
쿼리에 문법 오류가 발생하면, 컴파일시 확인 할 수 있는 것이다.

@Entity
@NamedQuery( 
    name = "Member.namedQuery"
    query = "select m from ~~~~"
public class Member{
}
em.createQuery("Member.namedQuery", Member.class)
    .setParameter("~~~", "~~")
    .gerResultList();

<br>

Spring Data JPA는 이를 한번 더 추상화 시켰는데,
다음과 같은 방식으로 사용한다.

publci interface MemberRepository extends JpaRepository<Member, Long> {

    @Query("select m from Member m ~~") // Named Query, 
    Member findByName(String name);
}

<br>

벌크연산

예를 들어, update, delete 쿼리를 10000개 쿼리에 요청하고 싶다면,
10000개의 데이터를 가져와, dirty checking 방식을 사용해
10000번의 쿼리를 요청해야한다.

이런 문제 때문에,
JPA도 한번의 쿼리로 해결 할 수 있는 방식을 제공하고
이를 벌크 연산이라 정의 한다.

em.createQuery("update Member m set m.age = 10");

<br>

벌크 연산을 사용함에 있어서, 주의해야 할 점을 꼭 기억해야한다.

Member m = new Member();
m.setAge(20);
em.persist(m);

em.createQuery("update Member m set m.age = 10");

Member savedM = em.findById(m.getId());
System.out.println(savedM.getAge()); /// 10??? 20???

해당 값의 결과는 10이 아닌, 20을 보여줄 것이다.

벌크 연산은 영속성 컨텍스를 무시하고,
데이터베이스에 직접 쿼리를 요청하기 때문이다.

실제로 벌크 연산 쿼리가 요청되어, 데이터 베이스의 값은 바뀌었을 것이다.
하지만, 영속성 컨텍스트 내의 1차 캐시 공간에, 값은 바뀌지 않았다.

이 때문에, 벌크 연산은 영속성 컨텍스트의 1차 캐시의 값과,
데이터베이스의 값의 데이터 불일치가 발생 하게된다.

따라서, 1차 캐시에 저장된 m을 리턴하게되고, 20이라는 값을 보여주게 되는 것이다.

<br>

그러므로, 벌크 연산을 사용함에 있어 다음 규칙중 하나를 꼭 준수하자.

1. 무조건 벌크 연산을 우선 수행한다.
2. 벌크 연산 이후에는, em.clear() // 1차캐시 초기화를 반드시 호출하자.

<br>

Spring Data Jpa는 이 문제를 @Modifying 어노테이션을 붙여줌으로 해결한다.
insert, update, delete 벌크 연산의 쿼리의 경우
@Modifying 어노테이션이 강제규약이되며, 없을경우 컴파일 에러가 발생한다.

@Modifying 어노테이션이 붙음으로써, 해당 쿼리가 벌크연산이라는 것을 명시하고, 쿼리 수행 후 영속성컨텍스트가 초기화된다.
(어노테이션의 cleareAutomatically 값을 통해 영속성컨텍스트를 초기화 안시킬수도 있다).

publci interface MemberRepository extends JpaRepository<Member, Long> {

    @Modifying
    @Query("update Member m set m.age = :age")
    Member updateMemberAge(Integer age);
}
0
이전 댓글 보기
등록
TOP