[10] 객체 지향 쿼리 언어

ttt-1-2·어제

교재: 자바 ORM 표준 JPA 프로그래밍 

10장에서 다룰 내용:

  • JPQL
  • Criteria
  • QueryDSL
  • 네이티브 SQL
  • 객체지향 쿼리 심화

1. 객체지향 쿼리 소개

JPA는 다양한 쿼리 방법을 지원한다. 핵심은 "테이블이 아닌 엔티티 객체를 대상으로 쿼리한다"는 점이다.

JPA가 지원하는 쿼리 방식:

  • JPQL: 가장 기본이 되는 객체지향 쿼리 언어
  • Criteria: JPQL을 자바 API로 작성하는 방식
  • QueryDSL: Criteria보다 훨씬 간결하고 직관적인 오픈소스 라이브러리
  • 네이티브 SQL: SQL을 직접 사용
  • JDBC 직접 사용: JDBC, MyBatis 등을 JPA와 함께 사용

실무에서는 JPQL을 기본으로 쓰고, 필요에 따라 Criteria나 QueryDSL을 조합한다.

2. JPQL

JPQL 문법은 SQL과 유사하다. SELECT, UPDATE, DELETE를 지원한다.

  • SELECT 기본 예시
// 기본 조회
SELECT m FROM Member AS m WHERE m.age > 18

TypeQuery, Query

em.createQuery()의 반환 타입에 따라 두 가지로 나뉜다.

TypeQuery: 반환 타입이 명확할 때

TypedQuery<Member> query =
    em.createQuery("SELECT m FROM Member m", Member.class);

List<Member> resultList = query.getResultList();
for (Member member : resultList) {
    System.out.println("member = " + member);
}

Query: 반환 타입이 여러 개일 때

Query query =
    em.createQuery("SELECT m.username, m.age FROM Member m");

List resultList = query.getResultList();
for (Object o : resultList) {
    Object[] result = (Object[]) o; // 결과가 여러 타입이면 Object[]
    System.out.println("username = " + result[0]);
    System.out.println("age = " + result[1]);
}

결과 조회 API

query.getResultList();    // 결과가 없으면 빈 컬렉션 반환
query.getSingleResult();  // 결과가 정확히 1개일 때 사용

파라미터 바인딩

이름 기준 파라미터 (권장):

List<Member> members =
    em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class)
      .setParameter("username", usernameParam)
      .getResultList();

위치 기준 파라미터 (비권장):

List<Member> members =
    em.createQuery("SELECT m FROM Member m WHERE m.username = ?1", Member.class)
      .setParameter(1, usernameParam)
      .getResultList();

이름 기준 파라미터를 사용하는 것이 훨씬 명확하고 유지보수에 유리하다. 위치 기준은 순서가 바뀌면 버그가 생기기 쉽다.

프로젝션 (Projection)

SELECT 절에서 조회할 대상을 지정하는 것을 프로젝션이라 한다.

// 엔티티 프로젝션 - 엔티티 전체 조회
SELECT m FROM Member m        // Member 엔티티
SELECT m.team FROM Member m   // 연관 엔티티 Team 조회
  • 임베디드 타입 프로젝션:
String query = "SELECT a FROM Address a";
// 단, 임베디드 타입은 독립적으로 조회 불가 → 엔티티를 통해 조회해야 함

String query = "SELECT o.address FROM Order o";
  • 스칼라 타입 프로젝션 (단순 값 조회)
// 단일 타입
List<String> usernames =
    em.createQuery("SELECT m.username FROM Member m", String.class)
      .getResultList();

// DISTINCT로 중복 제거
SELECT DISTINCT m.username FROM Member m

// 집합 함수
SELECT AVG(o.orderAmount) FROM Order o
  • 여러 값 조회: 반환 타입이 다를 경우 Object[]로 받는다:
// Object[] 방식
List<Object[]> resultList =
    em.createQuery("SELECT m.username, m.age FROM Member m")
      .getResultList();

for (Object[] row : resultList) {
    String username = (String) row[0];
    Integer age     = (Integer) row[1];
}
  • 명령어로 DTO 직접 조회 (권장) Object[]보다 DTO로 바로 받는 방식이 더 깔끔하다:
public class UserDTO {
    private String username;
    private int age;

    public UserDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }
}
TypedQuery<UserDTO> query =
    em.createQuery(
        "SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m",
        UserDTO.class
    );

페이징 API

JPA는 페이징을 두 가지 API로 추상화한다. DB마다 다른 페이징 SQL을 자동으로 생성해준다.

TypedQuery<Member> query =
    em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);

query.setFirstResult(10);  // 조회 시작 위치 (0부터 시작)
query.setMaxResults(20);   // 조회할 데이터 수
query.getResultList();     // 11번째부터 20개 조회

→ DB별로 생성되는 SQL이 다르다. JPA가 알아서 변환해준다.

집합과 정렬

  • 집합 함수
SELECT
    COUNT(m),    -- 회원 수 (반환: Long)
    SUM(m.age),  -- 나이 합 (반환: Long)
    AVG(m.age),  -- 평균 나이 (반환: Double)
    MAX(m.age),  -- 최대 나이
    MIN(m.age)   -- 최소 나이
FROM Member m
  • GROUP BY / HAVING
SELECT t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
FROM Member m LEFT JOIN m.team t
GROUP BY t.name
  • ORDER BY
SELECT t.name, COUNT(m.age) AS cnt
FROM Member m LEFT JOIN m.team t
GROUP BY t.name
ORDER BY cnt

조인 (JOIN)

  • 내부 조인 (INNER JOIN)
String query = "SELECT m FROM Member m INNER JOIN m.team t WHERE t.name = :teamName";

List<Member> members = em.createQuery(query, Member.class)
    .setParameter("teamName", "팀A")
    .getResultList();
  • 외부 조인 (LEFT OUTER JOIN)
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t

페치 조인 (Fetch Join)

페치 조인은 JPQL에서 성능 최적화를 위해 제공하는 특별한 조인이다. 연관 엔티티를 SQL 한 번에 함께 조회한다. 지연 로딩으로 설정해도 페치 조인을 사용하면 즉시 함께 로딩된다.

  • 엔티티 페치 조인:
// JPQL
SELECT m FROM Member m JOIN FETCH m.team

→ 실행 SQL:

SELECT M.*, T.*
FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID

일반 조인과 다르게, 페치 조인은 연관 엔티티까지 함께 SELECT 절에 포함해서 가져온다. member.getTeam()을 호출해도 추가 쿼리가 발생하지 않는다.

  • 컬렉션 페치 조인
select t from Team t join fetch t.members where t.name = '팀A'

→ 실행 SQL:

SELECT T.*, M.*
FROM TEAM T INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = '팀A'

일대다 조인이므로 팀A에 회원이 2명이면 결과 row가 2개 나온다. 즉 팀 객체가 중복으로 조회될 수 있다.

  • 페치 조인과 DISTINCT: 컬렉션 페치 조인 시 중복 결과를 제거하려면 DISTINCT를 사용한다.
select distinct t from Team t join fetch t.members where t.name = '팀A'
  • 일반 조인 vs 페치 조인 차이
// 일반 조인 → SELECT 절에 Team만 포함, members는 미로딩
select t from Team t join t.members m where t.name = '팀A'

// 페치 조인 → SELECT 절에 Team + Member 모두 포함
select t from Team t join fetch t.members where t.name = '팀A'

페치 조인 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다 (Hibernate는 가능하나 권장하지 않음)
  • 둘 이상의 컬렉션을 페치 조인할 수 없다 (MultipleBagFetchException 발생)
  • 컬렉션 페치 조인 시 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다 → 전체 데이터를 메모리에 올려 페이징하므로 위험

경로 표현식

.을 통해 객체 그래프를 탐색하는 것이다.

select m.username   // 상태 필드
from Member m
    join m.team t   // 단일 값 연관 필드
    join m.orders o // 컬렉션 값 연관 필드
where t.name = '팀A'

경로 표현식의 3가지 종류:

종류예시특징
상태 필드m.username, m.age더 이상 탐색 불가
단일 값 연관m.team묵시적 내부 조인 발생, 추가 탐색 가능
컬렉션 값 연관m.orders묵시적 내부 조인 발생, 추가 탐색 불가

서브쿼리

JPQL도 SQL처럼 서브쿼리를 지원한다. WHERE, HAVING 절에서 사용 가능하다 (SELECT, FROM 절에서는 불가).

조건식

JPQL에서 사용하는 기본 조건식들이다.

타입 표현:

종류예시
문자'Hello', 'She''s'
숫자10L, 10D, 10F
BooleanTRUE, FALSE
Enumjpabook.MemberType.Admin
엔티티 타입TYPE(m) = Member

상속 관계와 TYPE, TREAT

  • 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')
  • TREAT: 상속 구조에서 부모 타입을 자식 타입으로 다운캐스팅할 때 사용한다.
select i from Item i where treat(i as Book).author = 'kim'

사용자 정의 함수 (JPA 2.1)

DB 전용 함수나 커스텀 함수를 JPQL에서 사용하고 싶을 때:

// JPQL
select function('group_concat', i.name) from Item i

// Dialect에 등록
public class MyH2Dialect extends H2Dialect {
    public MyH2Dialect() {
        registerFunction("group_concat",
            new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
    }
}

엔티티 직접 사용

기본 키 값 대신 엔티티를 직접 파라미터로 사용할 수 있다. JPA가 자동으로 PK 비교로 변환한다.

// 엔티티를 직접 파라미터로
String qlString = "select m from Member m where m = :member";
List resultList = em.createQuery(qlString)
    .setParameter("member", member)  // 엔티티 직접 전달
    .getResultList();

// 실행 SQL → PK로 자동 변환
select m from Member m where m.id = ?

Named 쿼리: 정적 쿼리

JPQL은 동적 쿼리와 정적 쿼리로 나뉜다.

  • 동적 쿼리: em.createQuery("...")처럼 런타임에 문자열로 만드는 방식
  • 정적 쿼리 (Named 쿼리): 미리 이름을 붙여 정의해두는 방식 → 애플리케이션 로딩 시점에 문법 검증

@NamedQuery로 정의:

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

XML로 정의하는 방법도 있다:

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings>
    <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-mappings>

3. Criteria

JPQL을 자바 코드로 작성하는 API. 컴파일 시점에 오류를 잡을 수 있지만 코드가 복잡하다. 실무에서는 보통 QueryDSL을 더 선호한다.

기본 구조:

// JPQL: select m from Member m where m.username='회원1' order by m.age desc

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class); // FROM

cq.select(m)
  .where(cb.equal(m.get("username"), "회원1"))  // WHERE
  .orderBy(cb.desc(m.get("age")));              // ORDER BY

List<Member> resultList = em.createQuery(cq).getResultList();

CriteriaBuilderem.getCriteriaBuilder()로 얻는다. Root는 조회 시작점(FROM 절의 엔티티)이다.

조회 (SELECT)

단일 타입 조회:

cq.select(m);  // JPQL: select m

여러 값 조회 (multiselect):

// JPQL: select m.username, m.age
cq.multiselect(m.get("username"), m.get("age"));

// 또는 cb.array() 사용
cq.select(cb.array(m.get("username"), m.get("age")));

Tuple 조회 (별칭으로 접근):

CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<Member> m = cq.from(Member.class);

cq.multiselect(
    m.get("username").alias("username"),
    m.get("age").alias("age")
);

for (Tuple tuple : em.createQuery(cq).getResultList()) {
    String username = tuple.get("username", String.class);
    Integer age     = tuple.get("age", Integer.class);
}

GROUP BY / HAVING

// JPQL: select m.team.name, max(m.age), min(m.age) from Member m group by m.team.name

Expression<Integer> maxAge = cb.max(m.<Integer>get("age"));
Expression<Integer> minAge = cb.min(m.<Integer>get("age"));

cq.multiselect(m.get("team").get("name"), maxAge, minAge)
  .groupBy(m.get("team").get("name"))
  .having(cb.gt(minAge, 10));  // HAVING min(m.age) > 10

조인 (JOIN)

// JPQL: select m, t from Member m inner join m.team t where t.name = '팀A'

Root<Member> m = cq.from(Member.class);
Join<Member, Team> t = m.join("team", JoinType.INNER); // INNER / LEFT / RIGHT

cq.multiselect(m, t)
  .where(cb.equal(t.get("name"), "팀A"));
  • 페치 조인:
m.fetch("team", JoinType.LEFT);
cq.select(m);

파라미터 바인딩

// JPQL: select m from Member m where m.username = :usernameParam

ParameterExpression<String> usernameParam = cb.parameter(String.class, "usernameParam");
cq.select(m).where(cb.equal(m.get("username"), usernameParam));

em.createQuery(cq).setParameter("usernameParam", "회원1").getResultList();

Criteria는 타입 안전하고 동적 쿼리 작성에 강점이 있지만, 코드가 길고 가독성이 떨어진다. 실무에서는 QueryDSL이 같은 장점을 훨씬 간결하게 제공하므로 더 많이 사용된다.

동적 쿼리

Criteria의 가장 큰 강점. 조건에 따라 쿼리를 동적으로 조립할 수 있다.

  • JPQL 방식 (문자열 조합 → 지저분):
StringBuilder jpql = new StringBuilder("select m from Member m join m.team t");
List<String> criteria = new ArrayList<>();

if (age != null) criteria.add(" m.age = :age ");
if (username != null) criteria.add(" m.username = :username ");
if (teamName != null) criteria.add(" t.name = :teamName ");

if (!criteria.isEmpty()) jpql.append(" where ");
for (int i = 0; i < criteria.size(); i++) {
    if (i > 0) jpql.append(" and ");
    jpql.append(criteria.get(i));
}
  • Criteria 방식 (타입 안전, 깔끔):
List<Predicate> predicates = new ArrayList<>();

if (age != null) predicates.add(cb.equal(m.<Integer>get("age"), cb.parameter(Integer.class, "age")));
if (username != null) predicates.add(cb.equal(m.get("username"), cb.parameter(String.class, "username")));
if (teamName != null) predicates.add(cb.equal(t.get("name"), cb.parameter(String.class, "teamName")));

cq.where(cb.and(predicates.toArray(new Predicate[0])));

메타모델 API

m.get("age")처럼 문자열로 필드를 참조하면 오타가 생겨도 컴파일 에러가 안 난다. 메타모델을 사용하면 Member_.age처럼 타입 안전하게 참조할 수 있다.

// 메타모델 사용 전
m.<Integer>get("age")

// 메타모델 사용 후 (타입 안전)
m.get(Member_.age)

메타모델 클래스(Member_)는 어노테이션 프로세서가 자동 생성한다. 실무에서는 빌드 도구 설정으로 자동화한다.

4. QueryDSL

Criteria의 복잡함을 해결한 오픈소스 라이브러리. JPQL과 거의 같은 문법을 자바 코드로 간결하게 작성할 수 있다.

설정

<dependency>
    <groupId>com.mysema.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <version>3.6.3</version>
</dependency>

QueryDSL도 Q 클래스를 자동 생성해야 한다. mvn compiletarget/generated-sources에 생성된다.

기본 사용법

JPAQuery query = new JPAQuery(em);
QMember qMember = new QMember("m"); // Q 클래스 생성

List<Member> members = query
    .from(qMember)
    .where(qMember.name.eq("회원1"))
    .orderBy(qMember.name.desc())
    .list(qMember);
  • 기본 인스턴스 사용 (권장):
// static import 활용
import static jpabook.jpashop.domain.QMember.member;

List<Member> members = query
    .from(member)
    .where(member.name.eq("회원1"))
    .list(member);

검색 조건

// where 절에 and 조건 나열
query.from(item)
    .where(item.name.eq("좋은상품"), item.price.gt(20000))
    .list(item);

// 다양한 조건 메서드
item.price.between(10000, 20000)       // BETWEEN
item.name.contains("상품")             // LIKE '%상품%'
item.name.startsWith("좋은")           // LIKE '좋은%'

결과 조회

query.list(item);          // 리스트 반환, 없으면 빈 컬렉션
query.uniqueResult(item);  // 단건, 없으면 null, 2개 이상이면 예외
query.singleResult(item);  // 단건, 없으면 null (uniqueResult와 유사)

페이징 / 정렬

query.from(item)
    .where(item.price.gt(20000))
    .orderBy(item.price.desc(), item.stockQuantity.asc())
    .offset(10).limit(20)
    .list(item);

그룹

query.from(item)
    .groupBy(item.price)
    .having(item.price.gt(1000))
    .list(item);

5. 네이티브 SQL

  • JPQL이 아무리 강력해도 결국 한계가 있다. 특정 DB에서만 지원하는 기능, 예를 들어 오라클의 CONNECT BY나 SQL 힌트 같은 건 JPQL로 표현이 안 된다.

→ 이럴 때 쓰는 게 네이티브 SQL이다. JPA가 직접 SQL을 날릴 수 있도록 지원해주는 기능이다.

  • JDBC로 직접 SQL을 쓰는 것과 차이가 있다. 네이티브 SQL을 쓰면 영속성 컨텍스트 기능은 그대로 사용할 수 있다. 단점은 특정 DB에 종속적인 쿼리를 작성하게 되므로 DB를 바꾸면 SQL도 바꿔야 한다는 것이다.

엔티티 조회

String sql = "SELECT ID, AGE, NAME, TEAM_ID FROM MEMBER WHERE AGE > ?";

Query nativeQuery = em.createNativeQuery(sql, Member.class)
    .setParameter(1, 20);

List<Member> resultList = nativeQuery.getResultList();

값 조회

엔티티가 아닌 여러 컬럼 값을 그냥 받고 싶을 때는 두 번째 파라미터 없이 호출하면 된다.

String sql = "SELECT ID, AGE, NAME FROM MEMBER WHERE AGE > ?";

Query nativeQuery = em.createNativeQuery(sql)
    .setParameter(1, 20);

List<Object[]> resultList = nativeQuery.getResultList();

for (Object[] row : resultList) {
    String id   = (String) row[0];
    int    age  = (Integer) row[1];
    String name = (String) row[2];
}

결과 매핑 사용

엔티티와 스칼라 값을 함께 조회하는 복잡한 경우엔 @SqlResultSetMapping을 써서 매핑을 정의한다.

@SqlResultSetMapping(
    name = "memberWithOrderCount",
    entities = {
        @EntityResult(entityClass = Member.class)
    },
    columns = {
        @ColumnResult(name = "ORDER_COUNT")
    }
)
String sql = "SELECT M.ID, AGE, NAME, TEAM_ID, I.ORDER_COUNT "
           + "FROM MEMBER M "
           + "LEFT JOIN (SELECT IM.ID, COUNT(*) AS ORDER_COUNT "
           + "           FROM ORDERS O, MEMBER IM "
           + "           WHERE O.MEMBER_ID = IM.ID) I "
           + "ON M.ID = I.ID";

Query nativeQuery = em.createNativeQuery(sql, "memberWithOrderCount");

→ 복잡한 쿼리 결과를 엔티티 + 추가 컬럼으로 나눠서 받을 수 있다.

Named 네이티브 쿼리

JPQL처럼 이름을 붙여서 관리할 수 있다.

엔티티 클래스에 @NamedNativeQuery로 정의하고 em.createNamedQuery()로 사용한다.

@Entity
@NamedNativeQuery(
    name  = "Member.memberSQL",
    query = "SELECT ID, AGE, NAME, TEAM_ID FROM MEMBER WHERE AGE > ?",
    resultClass = Member.class
)
public class Member { ... }
TypedQuery<Member> nativeQuery =
    em.createNamedQuery("Member.memberSQL", Member.class)
      .setParameter(1, 20);

네이티브 SQL 정리

네이티브 SQL은 관리하기 쉽지 않다. 자주 쓰면 특정 DB에 종속적인 쿼리가 늘어나서 이식성이 떨어진다.

→ 최대한 JPQL을 쓰고, 방법이 없을 때만 네이티브 SQL을 쓰는 것이 맞다.

그래도 안 되면 MyBatis 같은 SQL 매퍼를 함께 도입하는 것도 방법이다.

6. 객체 지향 쿼리 심화

벌크 연산

벌크 연산은 한 번에 여러 엔티티를 수정하거나 삭제할 때 사용한다.

예를 들어 재고가 10개 미만인 모든 상품의 가격을 10% 올리고 싶다면, 엔티티를 하나씩 꺼내서 수정하는 건 비효율적이다.

// UPDATE 벌크 연산
String jpql = "update Product p "
            + "set p.price = p.price * 1.1 "
            + "where p.stockAmount < :stockAmount";

int resultCount = em.createQuery(jpql)
    .setParameter("stockAmount", 10)
    .executeUpdate();
// DELETE 벌크 연산
String jpql = "delete from Product p "
            + "where p.price < :price";

int resultCount = em.createQuery(jpql)
    .setParameter("price", 100)
    .executeUpdate();

executeUpdate()를 사용하고, 영향받은 엔티티 건수를 반환한다.

영속성 컨텍스트와 JPQL

  • JPQL 조회 시 동작 방식
DB 조회 결과 → 엔티티 확인
    ↓
이미 영속성 컨텍스트에 있음? → YES → 기존 엔티티 반환 (DB 결과 버림)
                              → NO  → 새 엔티티 영속성 컨텍스트에 저장 후 반환
  • JPQL과 플러시

JPQL 실행 전에 JPA는 자동으로 플러시를 한다. 영속성 컨텍스트에 변경 사항이 있는 상태에서 JPQL로 조회하면, 아직 DB에 반영되지 않은 내용이 누락될 수 있기 때문이다.

em.persist(new Product("상품X", 1000)); // 아직 DB에 없음

// JPQL 실행 직전에 자동 flush 발생
List<Product> results = em.createQuery("select p from Product p", Product.class)
    .getResultList();
// 상품X도 결과에 포함된다

FlushModeType.COMMIT 으로 설정하면 플러시 시점을 조정할 수 있다. 다만 이 경우 JPQL 조회 시 누락 문제가 생길 수 있으니 주의해서 사용해야 한다.

스토어드 프로시저 (JPA 2.1)

JPA 2.1부터 스토어드 프로시저 호출을 지원한다.

// 순서 기반 파라미터
StoredProcedureQuery spq =
    em.createStoredProcedureQuery("proc_multiply");

spq.registerStoredProcedureParameter(1, Integer.class, ParameterMode.IN);
spq.registerStoredProcedureParameter(2, Integer.class, ParameterMode.OUT);

spq.setParameter(1, 100);
spq.execute();

Integer out = (Integer) spq.getOutputParameterValue(2);
System.out.println("out = " + out); // 결과 출력

Named 스토어드 프로시저도 정의해서 재사용할 수 있다.

@NamedStoredProcedureQuery(
    name           = "multiply",
    procedureName  = "proc_multiply",
    parameters     = {
        @StoredProcedureParameter(name = "inParam",  mode = ParameterMode.IN,  type = Integer.class),
        @StoredProcedureParameter(name = "outParam", mode = ParameterMode.OUT, type = Integer.class)
    }
)
@Entity
public class Member { ... }
StoredProcedureQuery spq = em.createNamedStoredProcedureQuery("multiply");
spq.setParameter("inParam", 100);
spq.execute();

Integer out = (Integer) spq.getOutputParameterValue("outParam");

→ 이름 기반으로 관리하면 재사용하기 훨씬 편하다.

0개의 댓글