Dev/JPA

JPQL 의 개념과 간단한 사용 방법 ( 2 / 2 )

린네의 2024. 2. 17. 03:06

이전 포스팅에서 이어집니다.

2024.02.17 - [개발/jpa] - JPQL의 개념과 간단한 사용 방법 ( 1 / 2 )

 

JPQL 의 개념과 간단한 사용 방법 ( 1 / 2 )

게시글을 하나로 통합할까 하다가, 이론적인 부분과 실제 사용하는 구문을 분리하는 게 좋겠다는 생각이 들어서 두 개로 나눴다. 이 게시글은 JPQL 의 개념에 대한 내용이 주가 되었으니 참고 바

zigo-autumn.tistory.com

 

 

 

4. JPQL 에서의 페이징은 어떨까?

 JPA 에서 페이징은 setFirstResult(in statPosition )과 setMaxResults(int maxResult)로 추상화할 수 있다.

 

 각각의 API 의미는 다음과 같다.

  • setFirstResult(int startPosition) : 조회 시작 위치 
  • setMaxResults(int maxResult) : 조회할 데이터 수   
for (int i = 0; i < 100; i++) {

    Member member = new Member();
    member.setUsername("member" + i);
    member.setAge(i);
    em.persist(member);

}

em.flush();
em.clear();

List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(1)
        .setMaxResults(10)
                .getResultList();

System.out.println("result.isze = " + result.size());

for (Member member1 : result) {
    System.out.println("member1 = " + member1);
}

 

 위 코드를 실행하면,  1번째 행(row) 부터 10개의 행이 조회됨을 알 수 있다.  실제 쿼리는 persistene.xml 파일의  hibernate.idalect 에 지정된 dbms에 맞게 페이징 쿼리가 자동으로 실행된다 
 

실무에서 MariaDB(MySQL) 은 그렇다치고 Oracle을 사용할 때마다 코드가 길어지고 지저분해져서 보기 싫었는데 ( 항상 짤때마다 오라클 정도 되는 DBMS 가 왜 이렇게 페이징 지원에 야박한 지 원망함 ) 이 내용을 보고 정말 마음에 들었다.

 

 

5.  조인

 조인은 내부조인, 외부조인, 세타조인이 있다.  이 부분은 사실 SQL 과 거의 유사하기 때문에 간단한 예제만 작성했다.

내부, 외부 조인은 연관관계가 있어야하고 세타조인은 연관관계가 없어도 아무렇게나 사용할 수 있다는 차이점이 있다.

 

  • 내부 조인 
String query = "select m from Member m inner join m.team t";
    List<Member> result = em.createQuery(query, Member.class)
            .getResultList();

 

  •  외부 조인 
String query = "select m from Member m left join m.team t";
    List<Member> result = em.createQuery(query, Member.class)
            .getResultList();

 

  • 세타조인
String query = "select m from Member m, Team t where m.username = t.name";
            List result = em.createQuery(query, Member.class)
                    .getResultList();


        
 

ON 절 사용하기 

  • 조인 대상을 필터링 해준다.
  • 연관관계없는 엔티티 외부 조인을 지원한다. ( 하버네이트 5.1부터)


서브쿼리
  from 절의 서브 쿼리는 현재 jpql에서 불가능하다. (조인으로 풀 수 있으면 풀어서 해결하는데 풀어서 해결 안 되면 그냥 포기해야 한다 )


6.  기타 구문 - case,  coalesce, nullif

 

  • case 
String query = "select " +
            "case when m.age <= 10 then '학생요금'"
        + " when m.age >= 60 then '경로요금'"
        + " else '일반요금' end"
        + " from Member m";

List<String> result = em.createQuery(query, String.class).getResultList();
for (String s : result ) {
    System.out.println(" s = " + s);
}

 

 

  • coalesce ( 글자 합치기 ) 
String query = "select coalesce(m.username, '이름 없는 회원') from Member m";

            List<String> result = em.createQuery(query, String.class).getResultList();
            for (String s : result ) {
                System.out.println(" s = " + s);
            }

 

 

  • nullif ( nullif( a, b )에서  a 가 null 이면 b 값으로 치환 ) 
  String query = "select nullif(m.username, '관리자') from Member m";



            List<String> result = em.createQuery(query, String.class).getResultList();
            for (String s : result ) {
                System.out.println(" s = " + s);
            }

 

 

 

7.   fetch join ( 페치 조인 )과 N + 1 


N + 1 문제는 JPA 관련 게시글을 검색하면 정말 자주 따라오는 단골 키워드다. 

 

N + 1 문제는 사용자가 데이터를 조회하기 위해 최초로 실행한 쿼리가 1번 , 그 쿼리로 인해서 발생하는 ( 원하지 않은 ) 쿼리가 N 번 발생하는 것을 의미한다.

 

아래 예시를 보자.

    String query = "select m From Member m";

            List<Member> result = em.createQuery(query, Member.class).getResultList();

            for(Member me : result ) {
                System.out.println("member = " + member.getUsername() 
                + ", TeamNm :  "  + me.getTeam().getName()) ;
            }

 

select m From Member m 까지는 큰 문제가 없다. 그런데 sysout으로 출력하는 과정에서,  team의 내용이 필요하게 된다. 
이때  내부적으로 팀을 조회하기 위한 쿼리가 team 의 이름을 조회할 때마다 발생하게 된다. 즉, team 의 개수를 N 개 라고하면  N 번의 추가 쿼리가 발생하게 되는 것이다. 

 

사실 작은 데이터를 조회할 땐 큰 문제가 없겠지만, 이게 수십 건의 데이터가 엮인 실제 시스템에서 발생한다고 생각하면 끔찍한 일이 아닐 수 없다.  이럴 때 사용하는 것이  fetch join ( 이하 fetch join 혹은 페치 조인 )이다. 


다음은 페치 조인을 사용하여 개선된 코드이다.

String query = "select m From Member m join fetch m.team";

        List<Member> result = em.createQuery(query, Member.class).getResultList();

        for(Member me : result ) {
            System.out.println("member = " + member.getUsername() 
            + ", TeamNm :  "  + me.getTeam().getName()) ;
        }

 

 

사용자가 보는 결과는 ( 얻는 데이터는 ) 동일할지라도, 내부적으로는 단 하나의 쿼리만 실행되게 된다. 

페치 조인은 일반 조인과 차이점을 보이는데 즉시 로딩이라는 점과 한 번에 연관된 엔티티를 모두 묶어서 가져온다는 것이다.

 

좋아 보이는 페치조인도, 한계점은 분명히 있다. 아래에서 기술하는 한계점은 실제로 코드가 실행되지 않는 것은 아니지만, 개발자가 원하는 결과 값이 도출되지 않으므로 오류로 취급한다.

 

 

첫 번째,  페치 조인 대상에는 별칭을 사용할 수 없다.

 

사실 별칭을 못 쓰는 것은 아니다. 문법적으로 오류가 나진 않는데 실행 결과가 일관성을 잃는다.  아래코드를 실행하면 members 가 부분적으로만 출력되는 문제가 발생한다. ( 이때 team와 members 은 OneToMany를  가짐 ) 

  select t From Team t join fetch t, members as m

 

 여기서 member as m 이 별칭에 해당한다.  jpa는 members 가 다 출력된다는 가정하에 설계되었다는 걸 항상 염두에 두자. 

 

 둘째, 둘 이상의 컬렉션의 페치조인을 할 수 없다.

    일대다(@OneToMany)에서도 페치조인을 사용하면 리턴되는 데이터 개수가 늘어나는데 컬렉션끼리 해버리면 개수가 한 번에 엄청나게 증가할 수 있다.

 

셋째, 컬렉션을 페치 조인하면 페이징 api를 사용할 수 없다

 

  일대일(@OneToOne), 다대일(@ManyToOne) 같은 단일 값 연관 필드들은 페치조인을 해도 데이터가 증가하지 않아서 페이징 해도 문제가 없다.  만약 일대다가 문제가 될 경우에는, 다대일로 변경해서 사용하면 된다. 

 

 

  • 컬렉션 페치 조인 ( 일대다 관계일 때 조회하기, 팀 입장에서 멤버를 조회할 때  )

 조인을 하게 되면 데이터가 늘어나는 경우가 있다. 이럴 때 사용하는 것이 distinct이다.  distinct는 sql 단에서도 중복을 제거해 주고 리턴된 엔티티에 대해서도 중복을 제거해 주는 역할을 한다. 

( Hibernate 6부터는 중복제거가 기본으로 들어간다 ) 

 

 



8.   BatchSize 

 

일대다(@OneToMany)에서 컬렉션에 대해서 쓸 수 있는 애노테이션으로 , 사용 시 비슷한 쿼리들을 묶어 주는 역할을 한다.

 

예를 들어 team ( 1 )에 있는 members ( N )를 조회할 때, N의 수를 예상해서 BatchSize를 지정해 주면 1 + N 이 발생하지 않고   IN을 사용해서 동일한 team을 가진 members를 한 번에 묶는 쿼리를 날려준다. 

@BatchSize(size = 100 )


 
 persistene.xml 파일에 글로벌  세팅으로 주고 사용할 수 있다. 

 

 <property name="hibernate.default_batch_fetch_size" value="100" />

 



9.  기타 - @named와 벌크 연산 

 

  • @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();

 

 

  • 벌크연산 주의 

  벌크연산은 간단하게 설명하면,  데이터베이스에 쿼리 한 번을 실행해준다고 생각하면 된다. 주의해야 할 점은 영속성 콘텍스트를 건너뛰고 데이터베이스에 직접 접근해서 실행되기에, 연산 수행 후 영속성 컨텍스트를 초기화 ( em.clear() ) 해줘야 한다.   

 벌크연산 전의 데이터 ( 영속성 콘텍스트에 남아있는 데이터 )와 실행 후의 데이터 ( DB에 있는 값 )가 달라질 수 있기 때문이다.