Back-end/Spring

[Spring] JPA 동적 네이티브 쿼리, 제대로 알고 사용하자

류건 2024. 5. 22. 02:30
반응형

문제 상황

프로젝트를 수행하면서 다음과 같은 상황이었다.

 

cafe, beverage, document 세 엔티티에 대해 cafe : beverage = 1 : n 관계였으며, beverage : document = 1 : 1 관계를 갖는다.

 

세 개의 엔티티를 조인하고 특정 컬럼만 뽑아내기 위해 직접 CafeRepository에 다음과 같이 nativeQuery를 날린 상황이었다.

package com.alpha.DLINK.domain.cafe.repository;

import com.alpha.DLINK.domain.cafe.domain.Cafe;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface CafeRepository extends JpaRepository<Cafe, Long> {

    @Query(value = "SELECT b.beverage_id, d.content FROM Cafe c " +
            "JOIN Beverage b ON c.cafe_id = b.cafe_id " +
            "JOIN Document d ON b.document_id = d.document_id " +
            "WHERE :conditions", nativeQuery = true)
    List<Object[]> findBeverageIdAndDocumentByConditions(@Param("conditions") String conditions);

}

 

Cafe List에서 name과 latitude, longitude가 일치하는 것들만 데이터를 가져오는 쿼리가 필요해서 메인 서비스에서 직접 서브쿼리를 생성하여 해당 query문에 넣어주었다.

MainService.java

// 동적 네이티브 쿼리 생성
List<String> conditions = cafes.stream()
        .map(cafe -> String.format("(c.name = '%s' AND c.latitude = '%s' AND c.longitude = '%s')",
                cafe.getName(), cafe.getLatitude(), cafe.getLongitude())).toList();

String conditionsString = String.join(" OR ", conditions);

List<QueryResponseDTO> list = cafeRepository.findBeverageIdAndDocumentByConditions(conditionsString).stream().map(QueryResponseDTO::new).toList();

 

그랬더니 결과는 다음과 같았다.

 

2024-05-22T02:14:08.373+09:00 DEBUG 2667 --- [nio-8080-exec-2] org.hibernate.SQL                        : 
    SELECT
        b.beverage_id,
        d.content 
    FROM
        Cafe c 
    JOIN
        Beverage b 
            ON c.cafe_id = b.cafe_id 
    JOIN
        Document d 
            ON b.document_id = d.document_id 
    WHERE
        (
            ?
        )

 

Hibernate가 직접 작성한 조건 서브쿼리를 인식하지 못하는 문제가 발생한 것이다!

 

이를 어떻게 해야하나 곰곰히 생각해보고 검색해본 결과, 네이티브 쿼리를 사용할 때 조건을 문자열로 전달하는 방식은 SQL 인젝션의 위험이 있으므로 안전하게 처리해야 한다는 것을 알았다.

 

해결 방안

따라서 CafeRepositoryCustom 이라는 interface를 생성하고 해당 인터페이스의 구현체를 생성했다.

public interface CafeRepositoryCustom {
    List<Object[]> findBeverageIdAndDocumentByConditions(String conditions);
}
@Repository
public class CafeRepositoryCustomImpl implements CafeRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<Object[]> findBeverageIdAndDocumentByConditions(String conditions) {
        String sql = "SELECT b.beverage_id, d.content FROM Cafe c " +
                     "JOIN Beverage b ON c.cafe_id = b.cafe_id " +
                     "JOIN Document d ON b.document_id = d.document_id " +
                     "WHERE " + conditions;

        Query query = entityManager.createNativeQuery(sql);
        return query.getResultList();
    }
}

 

이후 사용하고자 하는 Spring Data JPA 구현체에 해당 인터페이스를 상속한다.

import org.springframework.data.jpa.repository.JpaRepository;

public interface CafeRepository extends JpaRepository<Cafe, Long>, CafeRepositoryCustom {
    // 다른 쿼리 메서드 정의
}

 

이렇게 변경하고 테스트해보았더니 정상적으로 작동했다!

 

Spring은 파도 파도 끝이 없는 것 같다. 공부할 것이 늘었다.

 

<추가> 트러블 슈팅

위의 동적 네이티브 쿼리 작성 시 로컬 환경에서는 정상 작동해도 EC2에서 다음과 같은 에러가 발생했다.

java.sql.SQLSyntaxErrorException: Table 'alpha.Cafe' doesn't exist
        at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:121) ~[mysql-connector-j-8.3.0.jar!/:8.3.0]
        at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-j-8.3.0.jar!/:8.3.0]
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:912) ~[mysql-connector-j-8.3.0.jar!/:8.3.0]
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeQuery(ClientPreparedStatement.java:968) ~[mysql-connector-j-8.3.0.jar!/:8.3.0]
        at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52) ~[HikariCP-5.0.1.jar!/:na]
        at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java) ~[HikariCP-5.0.1.jar!/:na]

 

알고보니 동적 네이티브 쿼리에서 JPQL 문법처럼 table 명을 엔티티 명으로 써버린 것이다!

 

@Repository
public class CafeRepositoryCustomImpl implements CafeRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<Object[]> findBeverageIdAndDocumentByConditions(String conditions) {
        String sql = "SELECT b.beverage_id, d.content FROM cafe c " +
                     "JOIN beverage b ON c.cafe_id = b.cafe_id " +
                     "JOIN document d ON b.document_id = d.document_id " +
                     "WHERE " + conditions;

        Query query = entityManager.createNativeQuery(sql);
        return query.getResultList();
    }
}

 

그래서 다음과 같이 테이블 명을 전부 소문자로 변경해보았다.

 

반응형