본문 바로가기
Framework and Tool/JPA

JPA - 객체지향 쿼리 언어 - JPQL 기본

by ocwokocw 2021. 7. 29.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- JPQL

개요 에서 엔티티를 조회하는 많은 방법들을 간단하게 살펴보았지만 어쨌든 출발점은 JPQL 이다. 이번 절에서는 JPQL 의 기본 사용법을 알아보자.

 

우선 JPQL 의 기본사항 부터 확인하고 넘어가도록 하자.

  • JPQL 은 SQL 이 아니라 객체 지향 쿼리 언어이다. 테이블을 대상으로 조회하는것이 아니다.
  • JPQL 은 특정 데이터베이스에 의존하지 않는다.
  • JPQL 은 결국 SQL 로 변환된다.

- 기본 문법

JPQL 도 SQL 과 비슷하게 SELECT, UPDATE, DELETE 를 사용할 수 있다. 엔티티 저장시에는 persist() 를 이용하므로 INSERT 문은 존재하지 않는다.

 

SELECT 문은 이미 살펴본적이 있다.

 

String jpql = "select m from Member m where m.name='efg'";
List<Member> members = em.createQuery(jpql, Member.class)
	.getResultList();

members.forEach(System.out::println);

 

SQL 만 사용해보았다가 JPQL 을 처음 사용하면 몇 가지 헷갈릴만한 사항이 있다.

  • 대소문자 구분: SQL 은 대소문자를 구별하지 않지만 JPQL 은 엔티티와 속성에 대해 대소문자를 구별해주어야 한다. 단, select 나 from 과 같은 예약어는 구분하지 않는다.
  • 엔티티 이름: @Entity 의 name 속성으로 엔티티 이름을 지정할 수 있다. 보통은 클래스명을 그대로 사용한다.
  • 별칭 필수: Member m 에서 별칭 m 은 SQL 에서는 필수가 아니지만 JPQL 은 별칭을 필수로 사용해야 한다.

반환 타입을 명확하게 지정할 수 있으면 TypedQuery 그렇지 않다면 Query 객체를 사용한다.

 

String jpql = "select m from Member m where m.name='efg'";
TypedQuery<Member> typedMemberQuery = em.createQuery(jpql, Member.class);

List<Member> members = typedMemberQuery.getResultList();

members.forEach(System.out::println);

 

위와 같이 Member 엔티티를 조회하면 반환 타입이 명확하므로 TypedQuery 를 이용할 수 있다.

 

String jpql = "select m.id, m.name from Member m where m.name='efg'";
Query memberQuery = em.createQuery(jpql);

List<?> members = memberQuery.getResultList();

members.forEach(obj -> {
	Object[] memberInfo = (Object[]) obj;
	System.out.println("id: " + memberInfo[0]);
	System.out.println("name: " + memberInfo[1]);
});

 

반면 Member 엔티티에서 id 와 name 만 조회한다고 하면 반환형이 명확하지가 않다. 이때는 Query 를 사용하여 조회한다. SELECT 절의 조회 대상이 2개 이상이면 Object[] 를 1개 이면 Object 를 반환한다.

 

Query 와 TypedQuery 중에는 당연히 TypedQuery 를 이용하는것이 더 편리하다. 참고로 createQuery 에서 TypeQuery 와 Query 를 반환하는 기준은 createQuery 의 2번째 인자에 Class 형을 인자로 주었느냐 여부에 따라 다르다.


- 파라미터 바인딩

JDBC 는 ? 와 index 순서로 파라미터 바인딩을 지원하지만 JPQL 은 이름을 기준으로 파라미터 바인딩을 지원한다. select 예제에서는 "efg" 고정값을 가진 회원을 조회하려고 했지만 보통은 변수가 될 가능성이 높다.

 

String username = "efg";
String jpql = "select m from Member m where m.name=:username";

List<Member> members = em.createQuery(jpql, Member.class)
		.setParameter("username", username)
		.getResultList();

members.forEach(System.out::println);

 

이름을 기준으로 파라미터 바인딩을 할 때에는 이름앞에 : 를 사용해준다. 그리고 setParameter 함수를 이용하여 JPQL에 지정한 이름과 바인딩할 값을 인자로 준다.

 

JPQL 에서 위치 기준 파라미터는 ?1 로 지정하고, setParameter 함수에서는 이름대신 1,2,3... 순서와 값을 바인딩을 해주면 된다. 하지만 이름 기준 파라미터가 더 명확하기 때문에 여기서는 굳이 살펴보지 않겠다.

 

파라미터 바인딩을 이용하지 않고 String + 연산을 이용해도 jpql 을 사용할 수 있지만 이렇게 하면 안된다. 우선 SQL Injection 공격에 취약하다.

 

또한 DB 에서는 파라미터만 다른 SQL 에 대해서는 기존 SQL 파싱 결과를 재사용하는데, 파라미터를 사용하지 않고 jpql 을 동적으로 구성하면 이를 이용하지 않는다. 이렇게 되면 WAS 옵션에는 보통 PreparedStatement 를 일정개수 및 용량으로 캐싱하는 기능이 있는데 이 기능도 무용지물이 된다.


- 프로젝션 기본

SELECT 절에 조회할 대상을 나타내는것을 프로젝션 이라고 한다. 프로젝션의 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다.

 

위의 말만 보면 복잡한 것 같지만 앞의 예제에서 이미 엔티티와 스칼라 타입에 대한 프로젝션 예제를 살펴보았다. 만약 SQL에서 * 를 사용하지 않고 회원 엔티티의 모든 정보를 조회한다고 하면 속성들을 모두 나열해야한다. 하지만 JPQL 에서는 select m from Member m 으로 Member 엔티티를 지정하여 select 할 수 있다. 이것이 바로 프로젝션의 대상을 엔티티로 지정하여 조회한것이다. 잘 생각해보면 투영이라는 사전적 정의와 어울리는 면이 있다.

 

스칼라 타입 프로젝션은 숫자, 문자, 날짜와 같은 기본 데이터 타입들을 조회하는것이다. select m.name from Member m 이라는 JPQL 은 스칼라 타입 프로젝션이다.

 

임베디드 타입 프로젝션은 이전 예제에서 본적이 없으므로 한번 코드를 작성해보도록 하자.

 

@Entity
public class Member extends DateMarkable{

	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;

	private String name;

	@Embedded
	private Address address;

.............

Member member2 = new Member();
member2.setName("efg");
member2.setAddress(new Address("City#1", "Street#1", "Zipcode#1"));
em.persist(member2);

..............

String username = "efg";
String jpql = "select m.address from Member m where m.name=:username";

List<Address> addressOfMember = em.createQuery(jpql, Address.class)
		.setParameter("username", username)
		.getResultList();

addressOfMember.forEach(System.out::println);

 

1번째 단락에서는 Member 엔티티는 Address 임베디드 타입을 포함하고 있다. 2번째 단락에서는 Address 값 타입을 정하여 member2 를 영속화하였다. 3번재 단락의 jpql 을 자세히 살펴보자.

 

select m.address 로 Address 형의 속성명인 address 지정하였다. 엔티티와 거의 비슷하게 사용되지만 차이점이 있다면 시작점으로는 사용될 수 없다. "select a from Address a" 와 같이 JPQL 을 구성할 수 없다는 뜻이다. 또한 엔티티 타입 프로젝션으로 조회한 엔티티는 영속성 컨텍스트에서 관리되지만 값 타입 은 관리되지 않는다.


- 프로젝션 응용

Query 예제에서 살펴보았듯이 엔티티의 특정값들만 조회하려고 하면 번잡한 변환과정을 거쳐야 한다. Generic 에 Object[] 로 변환하면 타입 캐스팅 과정이 조금 나아지긴하지만 여전히 유지보수 하거나 개발하기에 수용할만한 정도는 아니다.

 

String username = "efg";
String jpql = "select m.id, m.address from Member m where m.name=:username";

List<Object[]> members = em.createQuery(jpql)
	.setParameter("username", username)
	.getResultList();

members.forEach(row -> {
	Long id = (Long) row[0];
	Address address = (Address) row[1];
	System.out.println("ID: " + id + ", Address: " + address);
});

 

위 예제의 address 타입처럼 스칼라 타입뿐만 아니라 임베디드 타입이나 엔티티 타입도 조회가 가능하다. 이때에도 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

 

Object[] 형을 하나씩 변환해서 사용하고 싶지 않다면 DTO 를 사용하는 방법도 있다. JPQL 로 select 시 선언한 DTO 를 이용해서 반환을 받을 수 있다.

 

DTO 는 레이어간의 데이터를 변환해서 전달하는 오브젝트이다. 프로젝트에서 VO 와 DTO 를 크게 구분짓지 않고 사용하는 경우가 있는데 엄밀히 말하면 둘은 다르다. VO, DTO 관련해서는 클린 아키텍처나 DDD 를 참조하도록 하자.

 

public class MemberDTO {

	private Long id;
	private Address address;
	
	public MemberDTO() {
		super();
	}

	public MemberDTO(Long id, Address address) {
		super();
		this.id = id;
		this.address = address;
	}

..........

String username = "efg";
String jpql = "select new com.example.demo.member.dto.MemberDTO(m.id, m.address)"
		+ " from Member m where m.name=:username";

List<MemberDTO> members = em.createQuery(jpql, MemberDTO.class)
	.setParameter("username", username)
	.getResultList();

members.forEach(memberDto -> {
	Long id = memberDto.getId();
	Address address = memberDto.getAddress();
	System.out.println("ID: " + id + ", Address: " + address);
});

 

Object 를 변환하는 대신 MemberDTO 를 하나 선언해서 적절한 생성자를 정의하였다. select 에서 마치 클래스의 인스턴스를 생성하듯이 new 를 이용해서 MemberDTO 에 담아준다. 한 가지 주의할점은 MemberDTO 의 패키지명을 포함한 클래스 명을 입력해주어야 한다.

댓글