JPA - 객체지향 쿼리언어(6)

DevSeoRex·2022년 12월 15일
0
post-thumbnail

🎈 What? 다형성 쿼리가 뭐죠 ❓

Java를 공부하면서 본 다형성하고 같은 것일까요..?
제가 알고 있는 부모의 참조 변수로 자식객체를 다루는 그 다형성이 맞나요??


다행히도 맞는 것 같습니다. 지금부터 다형성 쿼리에 대해서 살펴보겠습니다.

JPQL을 사용해서 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회합니다.
JPA에는 상속관계 매핑이 있는데 이것을 이용한 예시 코드를 아래에 작성해보겠습니다.
상속관계 매핑에서는 전략이 세 가지 있는데, 아직 상속관계 매핑에 대해서 공부하지 못한분들은 이 게시글을 참고하시면 좋을 것 같습니다.

// 상속관계 조회 예제
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item { ... }

@Entity
@DiscriminatorValue("B")
public class Book extends Item {
	...
    private String author;
}

// Album, Movie 생략

아래와 같은 코드를 실행하면 Item의 자식도 함께 조회되게 됩니다.

// Item을 조회하는 코드
List resultList = 
		em.createQuery("select i from them i").getResultList();

상속관계 매핑에서는 3가지 전략이 있었습니다.

  • 단일 테이블 전략
  • 조인 전략
  • 구현 클래스마다 테이블 전략

책에는 나오지는 않지만 저자이신 김영한님 강의를 보면 구현 클래스마다 테이블 전략을 사용하면 망한다고 했던 것이 기억나네요.


이렇게 되고 싶지 않다면 구현 클래스마다 테이블 전략을 사용하는 것은 권장하지 않는다고 합니다 🤣🤣

🦄 Type - 특정 자식 타입으로 타입을 한정해서 조회

부제목과 같이 TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용합니다.

// Item중에 Book과 Movie를 조회하는 예제

// JPQL
select i from Item i
where type(i) IN (Book, Movie)

// SQL
SELECT i FROM Item i
WHERE i.DTYPE in ('B', 'M')

예를 들어, Item과 Movie & Book & Game이 상속관계를 가지고 있다고 하겠습니다.
그럴 경우에 Game을 제외한 Movie와 Book만 조회를 해야하는 경우가 있을 수 있습니다.
그럴때 조회 대상을 특정 자식 타입으로 한정하기 위해 TYPE을 사용합니다.

🎃 TREAT - 부모 타입을 특정 자식 타입으로 다뤄보자!

갑자기 왜 호박 이모지를 넣게 되었을까요?
초등학교 시절 어학원을 다닐때 할로윈 데이마다 불렀던 노래가 있었는데 'treat or trick smell my feet~' 노래를 부르며 사탕을 받곤 했었죠

네.. 잠깐 그 어린 시절 제 귀엽던 모습이 생각나서.. 😂

본론으로 돌아가서, TREAT는 JPA 2.1에 추가된 기능인데요. 자바의 타입 캐스팅과 유사합니다.
상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용합니다.
사용 가능한 범위는 JPA 표준 스펙의 경우 FROM,WHERE 절에서 사용이 가능하고, 하이버네이트의 경우에는 SELECT 절에서도 사용할 수 있습니다.

부모인 Item 타입을 자식 타입인 Book으로 다루는 예제를 보도록 하겠습니다.

// 자식 타입으로 부모 타입을 다루는 예제 - TREAT

// 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을 보게 되면, treat를 사용해서 부모 타입인 Item을 자식 타입인 Book으로 다루는 것을 볼 수 있습니다. 따라서 author 필드에도 접근이 가능합니다.

😎 내가 만든 함수 - 사용자 정의 함수

컴활같은 자격증을 따게 되면 사용자 정의함수라는 것이 있는데요.
VBA를 이용해서 엑셀 기본함수 대신 내가 정한 이름과 정의한 동작으로 호출되는 함수입니다.
JPA에도 2.1 버전부터 사용자 정의 함수를 지원하게 되었는데요. 어떻게 함수를 정의하고 사용하는지 아래에서 살펴보도록 하겠습니다.

하이버네이트 구현체를 사용하는 경우에는 아래와 같이 방언 클래스를 상속해서 구현하고 사용할 데이터베이스 함수를 미리 등록해야 합니다.

// 방언 클래스 상속 예제
public class MyH2Dialect extends H2Dialect {
	
    public MyH2Dialect() {
    	registerFunction("group_concat", new StandardSQLFunction
        	("group_concat", StandardBasicType.STRING));
    }
}

위와 같이 방언 클래스를 상속받은 클래스에 함수를 정의한 뒤에는 상속한 방언을 persistence.xml 파일에 등록해야 정의한 함수를 사용할 수 있습니다.

<-- 상속한 방언 클래스 등록(persistence.xml) !-->
<property name="hibernate.dialect" value="hello.MyH2Dialect" />

이렇게 등록한 함수는 하이버네이트 구현체의 경우 아래와 같이 축약해서 사용이 가능합니다.

select group_concat(i.name) from Item i

🤯 NULL의 정의 & 논리 계산 등의 정리

비교에 대해서

  • enum= 비교 연산만 지원합니다.
  • 임베디드 타입은 비교를 지원하지 않습니다.

빈 문자열에 대해서(EMPTY STRING)
JPA 표준은 ''을 길이 0인 빈문자열로 정했지만, 사용하는 데이터베이스에 따라서 ''을 NULL로 사용하는 데이터베이스도 있기 때문에 확인하고 사용해야 합니다.

NULL 정의

  • 조건을 만족하는 데이터가 하나도 없으면 NULL 입니다.
  • NULL은 비어있는 값이 아니라, 알 수 없는 값 입니다. NULL과의 모든 수학적 계산 결과는 NULL 입니다.
  • Null == Null은 알 수 없는 값입니다.
  • Null is Null은 참입니다.

논리 계산에 대해서
JPA 표준 명세는 Null(U) 값과 TRUE(T), FALSE(F)의 논리 계산을 아래와 같이 정리했습니다.

  • AND 연산 정리
ANDTFU
TTFU
FFFU
UUFU
  • OR 연산 정리
ORTFU
TTTT
FTFU
UTUU
  • NOT 연산 정리
NOT
TF
FT
UU

👻 엔티티를 직접 사용가능 할까?

JPQL을 사용할때 파라미터를 셋팅해서 엔티티를 조회할 수 있었습니다.
그런데 만약 파라미터에 엔티티 자체를 값으로 제공한다면 어떤 일이 일어날까요?

객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본 키 값으로 식별합니다.
따라서, JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용하게 됩니다. 아래의 예제로 살펴 보겠습니다.

// JPQL에서 엔티티의 기본 키 값을 사용
select count(m.id) from Member m 

// JPQL에서 엔티티를 직접 사용
select count(m) from Member m

첫 번째 JPQL에서는 엔티티의 기본 키 값을 사용했고, 두 번째 JPQL에서는 엔티티를 직접 사용했습니다. 이렇게 두가지 방법으로 JPQL을 사용했는데 실행되는 SQL은 같습니다.

// 실행되는 SQL
select count(m.id) as cnt
from Member m

JPQL을 직접 작성했을때는 위와 같이 엔티티를 사용할때와 엔티티의 기본 키 값을 사용할때 동일한 SQL문이 실행되었습니다.
그렇다면, 엔티티를 파라미터로 받아서 실행할때도 동일한 결과가 나오는지 살펴보겠습니다.

// 식별자 값을 직접 사용(엔티티의 기본 키 값 사용)
String qlString = "select m from Member m where m.id = :memberId";
List resultList = em.createQuery(qlString)
		.setParameter("memberId", 4L)
        .getResultList();
        

// 엔티티를 직접 사용
String qlString = "select m from Member m where m.id = :member";
List resultList = em.createQuery(qlString)
		.setParameter("member", member)
        .getResultList();

이렇게 파라미터를 이용해서 JPQL을 사용해도 위의 두 코드는 같은 SQL문을 실행합니다.

select m.*
from Member m
where m.id = ?

엔티티를 직접 사용하더라도, SQL에서는 where m.id = ?로 변환되어 기본 키 값을 사용하도록 변환된 것을 확인할 수 있습니다.

🎯 외래 키 값에서도 엔티티를 사용해도 될까?

외래 키값에서도 파라미터로 엔티티와 식별자 값을 주었을 때 어떤 SQL이 실행되는지 확인해 보겠습니다.

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", 1L)
        .getResultList();

위의 코드를 실행하면 동일한 아래의 SQL이 실행 됩니다.

select m.*
from Member m
where m.team_id = ? (팀 파라미터의 ID 값)

위의 예제에서 m.team.id를 보면 Member와 Team 간에 묵시적 조인이 일어날 것 같지만 team_id 외래키는 MEMBER 테이블이 가지고 있으므로 묵시적 조인은 일어나지 않습니다.

당연히 m.team.name을 호출하게 되면 Team에 접근하는 것이므로, 묵시적 조인이 일어나게 됩니다.

✨ Named Query - 정적 쿼리는 무엇일까 ❗

JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있습니다.

  • 동적 쿼리 : em.CreateQuery("select .. ")처럼 JPQL을 문자로 완성해서 직접 넘기는 것을 동적 쿼리라고 합니다. 런타임에 특정 조건에 따라 JPQL을 동적으로 구성할 수 있습니다.
  • 정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을 Named 쿼리라고 합니다. Named 쿼리는 한 번 정의하면 변경할 수 없는 정적인 쿼리 입니다.

Named? 쿼리도 일반 쿼리보다 더 유명한 쿼리가 있었던가요?

웃자고 한 이야기입니다 🤣 저만 웃긴건 안 비밀
Named 쿼리, 즉 직역하면 이미 이름이 지어진 쿼리라는 의미겠죠. Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해 둡니다.

이것이 굉장히 큰 장점이라고 볼 수 있습니다.
오류를 빨리 확인할 수 있고, 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상의 이점까지 누릴 수 있습니다.
Named 쿼리는 변하지 않는 정적 SQL이 생성되는 것이므로, 데이터베이스의 조회 성능 최적화에도 도움이 됩니다.

그러면 이제부터 Named 쿼리를 어떻게 사용하는지 알아보겠습니다.

💯 Named 쿼리 정의 방법 - 애너테이션에 정의

Named 쿼리는 @NamedQuery 애너테이션을 사용해서 정의할 수 있습니다.
Entity 클래스에 직접 적용하면 되는데요. 아래의 예제로 살펴보겠습니다.

// Named Query 정의 예제 - 애너테이션 사용
@Entity
@NamedQuery(
	name = "Member.findByUsername",
    query = "select m from Member m where m.username =: username")
public class Member {
	...
}

위의 예제와 같이 클래스위에 @NamedQuery 애너테이션을 붙이고 속성으로 쿼리의 이름과 SQL 문장을 직접 부여해주면 정의가 끝납니다.

그러면 Named 쿼리는 어떻게 사용하는지 아래의 예제로 마저 살펴보겠습니다.

// Named Query 사용 예제
List<Member> resultList = em.createNamedQuery("Member.findByUsername",
	Member.class)
    	.setParameter("username", "회원1")
        .getResultList();

Named 쿼리를 사용하는 방법은 em.createNamedQuery( ) 메서드에 Named 쿼리 이름을 입력하면 됩니다. 파라미터가 있을 경우 setParameter( ) 메서드를 이용해 값을 입력해주면 일반적인 JPQL과 똑같이 사용할 수 있습니다.

🚦 Named 쿼리 이름을 Member.findByUsername으로 지은 이유는, Named 쿼리는 영속성 유닛 단위로 관리되기 때문에 충돌을 방지하기 위해 엔티티 이름을 앞에 붙여 줌으로서 관리하기 쉬워지기 때문입니다.

한 엔티티에 여러개의 Named 쿼리를 정의할 일도 분명히 생길 수 있습니다 그럴때는 어떻게 해야 할까요? 아래의 예제로 한 번 살펴보겠습니다.

// @NamedQueries 사용 예제 - Named 쿼리를 여러 개 정의
@Entity
@NamedQueries({
	@NamedQuery(
    	name = "Member.findByUsername",
        query = "select m from Member m where m.username = :username"),
    @NamedQuery(
    	name - "Member.count",
        query = "select count(m) from Member m")
    )    
})
public class Member { ... }

@NamedQueries 애너테이션을 사용해서 여러개의 Named 쿼리를 등록할 수 있습니다.

😋 @NamedQuery 너의 내부가 궁금해 ❗

우리가 사용하는 @NamedQuery 애너테이션은 어떻게 정의되어 있을까요?

// @NamedQuery 애너테이션의 실제 소스
@Target({TYPE})
public @interface NamedQuery {

	String name(); 							// Named 쿼리 이름 (필수)
    String query(); 						// JPQL 정의 (필수)
    LockModeType lockMode() default NONE;  // 쿼리 실행 시 락모드를 설정가능
    QueryHint[] hints() default {}; 	   // JPA 구현체에 쿼리 힌트를 줄 수 있다.
}

쿼리의 이름과 JPQL을 정의하는 속성은 사용해 보았지만 처음보는 두가지 속성이 있습니다. 이 속성들은 어디에 사용하는 것들인지 알아보겠습니다.

  • lockMode : 쿼리 실행 시 락을 겁니다.
  • hints : 여기서 말하는 힌트는 SQL 힌트가 아니라 JPQ 구현체에게 제공하는 힌트를 말합니다. 예를 들어 2차 캐시를 다룰 때 사용합니다.

🛴 Named 쿼리 정의 방법 - XML 파일에 정의

Named 쿼리를 정의하는 두 번째 방법은 XML 파일에 정의하는 방법이 있습니다.
Java에서 멀티라인 문자를 다루는 것은 상당히 귀찮은 일입니다.
SQL 문장이 길어져서 개행을 해야하는 경우에 애너테이션을 사용해서 정의한다면 아래와 같은 코드를 작성해야만 합니다.

// Named Query 정의 예제 - 애너테이션 사용(긴 SQL문장)
@Entity
@NamedQuery(
	name = "Member.findByUsername",
    query = "select " + 
    	"case t.name when '팀A' then '인센티브110%' " + 
        "			 when '팀B' then '인센티브120%' " + 
        "			 else '인센티브105%' end " + 
        "from Team t")
public class Member {
	...
}

보기만 해도 정말 어질 어질한 코드가 작성이 됩니다.
자바에서 이런 불편함을 해결하기 위한 그나마 현실적인 대안이 XML 파일을 사용하는 것입니다. ormMember.xml 파일을 작성해서 Named 쿼리를 정의해보겠습니다.

<!-- XML 파일로 Named 쿼리 정의 예제-->
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
  version="2.1">

<named-query name="Member.findByUsername">
  <query><CDATA[
          select m
          from Member m
          where m.username = :username
          ]></query>
</named-query>
  
<named-query name="Member.count">
  <query>select count (m) from Member m</query>
</named-query>
  
</entity-mapping>
  
  

Naemd 쿼리를 XML 파일에 정의한 뒤에는 ormMember.xml을 인식하도록 META-INF/persistence.xml에 아래의 코드를 추가해주면 됩니다.

<persistence-unit name="현재 유닛이름">
  <mapping-file>META-INF/ormMember.xml</mapping-file>

🔔 META-INF/orm.xml 이외의 파일은 persistence.xml에 설정 정보를 추가해야 인식이 가능합니다. orm.xml 파일도 위치가 META-INF가 아니라면 설정을 추가해야 합니다.

만약 같은 설정을 애너테이션과 XML에 같이 했다면 충돌이 일어날까요?

같은 설정을 했다면 XML이 우선권을 가집니다.
즉 충돌은 일어나지 않는다는 뜻입니다. XML을 활용하는 예시로는 애플리케이션이 운영 환경에 따라 다른 쿼리를 실행해야 한다면 각 환경에 맞춘 XML을 준비해두고 XML만 변경해서 배포하면 유연히 대처 할 수 있을 것입니다.

JPA는 아직도 신기한 걸.. 🤭

JPA 공부를 시작한지도 두 달 정도 된 것 같네요..
해도 해도 새로운 것 투성이고 아직 활용을 제대로 할 수 있을지 생각이 들지만 기술의 세계는 신기하고 재밌는 것이 많은 것 같습니다.

이제 JPA 활용편 강의를 회사 계정으로 듣게 되었는데 열심히 학습해서 미니 프로젝트도 하나 해보고, 얻은 Insights를 벨로그에 공유 하도록 하겠습니다.

출처 : 자바 ORM 표준 JPA 프로그래밍(에이콘 출판사 , 김영한 저)

0개의 댓글