• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

특정 칼럼에 대한 조회시 TypedQuery 사용에 대하여

24.02.08 18:02 작성 조회수 126

2

안녕하세요, 예전에 수업들은 이후 한창 현업 중에 잘못 사용한 Case 이지만, 왜 그럴까 디버깅도 좀 해보고 생각을 많이 해보다가 해결을 못하고 질문을 남기게 되었습니다. 그나마 관련이 있는 부분을 찾아서 남기게 되었습니다.

 

앱에는 Foo 라는 엔티티가 있고, 우선 다음과 같은 JPQL 쿼리를 발생시켰습니다.

String jpql = 
    "SELECT f.id FROM Foo f " + 
    "WHERE f.name = :name " + 
    "ORDER BY f.createdAt DESC" 

// id 라는 특정 칼럼 조회이므로 Type 을 명시하지 않고 Query 를 써야 하는게 맞지만, 잘못 사용해서 TypedQuery 를 사용함

List<Foo> fooList = em.createQuery(jpql, Foo.class).setParameter("name", name).getResultList();

 

일단 예외가 나지 않고 fooList 라는 List 가 반환된다는 것도 신기했습니다. 정확한 이해를 하지는 못했지만 디버깅 해본 결과, CgLibAopProxy 클래스들을 타는걸보면, Lazy 로딩과 유사하게 CustomFoo extends Foo 와 같은 상속객체를 만들어서, id 만 채워준 후 List 에 넣어서 반환해주기 때문에 문법적으로 fooList 를 만들 때 오류가 발생하는게 아니구나 판단했습니다. (아닐 수도 있습니다.. 제 생각)

 

이후 fooList.get(index) 를 시도하면서 바로 Casting 예외가 발생하였습니다. (Long 객체를 Foo 객체로 Casting 할 수 없다)

 

신기해서 IntelliJ 에서 디버깅 하면서 확인했을 때, fooList 라는 List 안에 Foo 객체들이 들어 있지 않고, Long 객체들이 들어 있는 모습을 확인했습니다. 이거는 좀 매우 이상한게, 어쨌든 Foo 라는 Type 이 들어가게끔 선언된 List 에 어떻게 Long 객체들만 들어가 있을 수 있는지가 이해가 되지 않았습니다.

 

createQuery() 함수와 getResultList() 함수간 복잡한 제네릭 관계들을 이해하기가 어려워서 이 부분을 이해하지 못한 것 같기도 합니다.

 

물론 TypedQuery 를 사용하지 않고 Query 를 사용하거나 DTO 를 통해 매핑해주는 것이 맞지만, 지금 상황 기준으로

 

  1. 어떠한 이유로 List 형성 중 에러가 나지 않은 것인지

  2. 또 어떻게 List<Foo> 안에 인텔리제이 디버깅 기준으로 Long 객체들이 들어가 있을 수 있는 것인지 궁금합니다




    _-----------------------------
    추가로 나중에 subQuery 안에서도 동일하게 사용이 되었는데, 이런 경우는 쿼리 결과를 반환해주지 않고 그냥 쿼리문을 만들 때만 사용되어서 그런지 아무 에러가 발생하지 않았습니다.

 

SELECT SUM(~~.~~) from Bar bar 
WHERE bar.foo.id in (
    SELECT f.id FROM Foo f 
    ~~
)

 

답변 2

·

답변을 작성해보세요.

3

인프런 AI 인턴님의 프로필

인프런 AI 인턴

2024.02.10

안녕하세요, 인프런 AI 인턴입니다.

강우석님의 질문에 대해 답변드립니다.

JPA에서 TypedQuery는 결과 타입을 명시적으로 지정할 때 사용되며, 결과 타입을 잘못 지정했음에도 불구하고 별다른 예외 없이 List를 반환하는 이유는 JPA 구현체가 실제 쿼리 수행 시점이 실행 시점(Runtime)이기 때문입니다. 즉, JPQL 쿼리를 준비하는 단계에서는 실제 쿼리가 수행되지 않고, 쿼리 메서드(getResultList())를 호출할 때 쿼리가 실행됩니다. 따라서 createQuery(jpql, Foo.class)라고 작성했을 때 컴파일 시점에서는 문제가 없다고 판단하게 됩니다.

그러나 실제 쿼리 결과는 f.id 즉, Long 타입이며 이는 Foo 타입이 아닙니다. 그래서 쿼리의 결과 리스트 내부에는 Long 객체들이 담기게 됩니다. 그런데, 해당 리스트를 List<Foo>로 받으려고 시도하였기 때문에, 리스트의 제네릭 타입이 일치하지 않는 문제가 발생합니다.

IntelliJ의 디버깅을 통해 리스트 내부에 Long 객체들만 있다는 것을 확인한 것은, 실제 쿼리의 결과가 Long 타입임에도 불구하고 타입에 맞지 않게 List<Foo>로 받아 처리하려 했기 때문입니다. 즉, JPA 구현체가 타입 체크를 런타임에 하기 때문에 이런 현상이 발생한 것으로 보입니다.

이러한 문제를 피하기 위해서는 반환되는 컬럼의 타입에 맞는 적절한 반환 타입으로 TypedQuery를 생성해야 합니다. f.id의 경우 Long 타입이므로 TypedQuery<Long>을 사용해야 합니다.

  1. List 형성 중에 에러가 나지 않은 것은, List<Foo>로 선언하는 것이 실제 내부 구현과 독립적으로 이루어졌기 때문입니다. 구체적인 실행 및 타입 확인은 런타임에 수행되며, 따라서 제네릭 타입이 잘못되었다 하더라도 컴파일 시점에서는 에러가 나지 않습니다.

  2. List<foo> 안에 Long 객체들이 들어간 것은 컴파일러가 실제 타입을 강제할 수 있는 방법이 없기 때문입니다. 자바의 제네릭 타입은 타입 소거(Type Erasure)가 적용되어 런타임에는 모든 제네릭 타입 정보가 소거되어 원시 타입으로 처리되기 때문입니다. 결과적으로는 List에 잘못된 타입의 객체가 들어갈 위험이 있으며, 이를 사용하는 측에서 올바르게 처리하지 않으면 <code>ClassCastException</code>을 던지게 됩니다.</foo>

즉, 올바르게 타입을 처리하지 않고 TypedQuery<Foo>로 쿼리를 생성한 상황에서 예외 없이 통과된 것은 컴파일 시점과 런타임 타입 체크의 타이밍 차이 때문이며, 실제 쿼리 결과 타입 (Long)과 선언한 타입 (Foo)이 일치하지 않아서 발생한 문제입니다.

항상 쿼리를 작성할 때 반환되는 컬럼과 결과 타입이 일치하도록 주의하여, 올바른 타입의 TypedQuery를 사용해야 합니다.

1

안녕하세요. 강우석님

이 부분은 자바의 제네릭에 대한 이해가 필요한데요.

AI 인턴의 답변을 참고해주세요.

감사합니다.