-
JPA @Query, Native Query, Converter백엔드 2023. 9. 5. 17:37728x90반응형SMALL
@Query
@Query는 쿼리 메서드의 커스텀 버전입니다.
쿼리메서드만으로 거의 모든 조회 쿼리를 작성할 수 있지만, 2가지 경우에서 @Query가 중요한 역할을 할 수 있습니다.
1️⃣ 첫 번째는 쿼리 메서드의 가독성에 문제가 생길 경우입니다.
쿼리 메서드의 이름이 길어질 경우 사용이 어려워질 수 있습니다.
public interface BookRepository extends JpaRepository<Book, Long> { ... List<Book> findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual(String name, LocalDateTime createdAt, LocalDateTime updatedAt); }
위와 같은 쿼리 메서드는 이름이 너무 길어 가독성이 떨어지게 됩니다.
이 때 @Query를 사용하여 간단한 메서드를 만들 수 있습니다.
public interface BookRepository extends JpaRepository<Book, Long> { ... @Query(value = "select b from Book b " + "where name = ?1 and createdAt >= ?2 and updatedAt >= ?3 and category is null") List<Book> findByNameRecently(String name, LocalDateTime createdAt, LocalDateTime updatedAt); }
위와 같이 @Query의 value에 JPQL 문법을 맞춘 쿼리문을 넣어주게 될 경우 위의 쿼리 메서드와 동일하게 동작하게 됩니다.
JPQL 쿼리는 JPA의 Entity를 기반으로 하는 쿼리를 생성할 수 있습니다. 위의 JPQL 쿼리문 내의 Book은 실제 Book Entity로 쿼리문 내에서도 대문자로 작성된 것을 확인할 수 있습니다.
또한, where 절의 name, createdAt 또한 실제 DB의 컬럼 명이 아닌 Entity의 필드에 있는 이름으로 작성이 됩니다.
JPQL은 Dialect를 통해 DB 종류에 따라 다른 쿼리를 자동으로 생성해줍니다.
value의 JPQL 내에 ?<숫자>와 같은 형식으로 넣어줄 경우 메서드의 인자 중 숫자 번째의 인자를 가져와 넣어주게 됩니다. (1 base index)
하지만, 인자값에 순서가 바뀌거나 다른 인자가 추가될 경우 혼동이 올 수 있기 때문에 순서에 의존성을 가지는 파라미터 활용은 지양해야 됩니다. 아래와 같은 방식으로도 메서드의 인자값을 JPQL 쿼리 내에 넣어줄 수 있습니다.
@Param 활용
public interface BookRepository extends JpaRepository<Book, Long> { ... @Query(value = "select b from Book b " + "where name = :name and createdAt >= :createdAt and updatedAt >= :updatedAt and category is null") List<Book> findByNameRecently( @Param("name") String name, @Param("createdAt") LocalDateTime createdAt, @Param("updatedAt") LocalDateTime updatedAt); }
위와 같이 선언할 경우 인자의 순서에 의존적이지 않고 로직의 변경에서 자유로워지기 때문에 유지보수에 좋을 것으로 생각됩니다.
2️⃣ 두 번째 경우는 Entity에 연결되지 않은 쿼리가 가능하기 때문입니다.
DB 테이블에 많은 컬럼 중 원하는 컬럼만 조회하는 것이 가능합니다.
Book Entity의 name과 category 속성만을 조회해보고 싶은 상황을 가정해보도록 하겠습니다.
1) interface 활용 방법
public interface BookInformation { String getName(); String getCategory(); }
getName()메서드와 getCategory() 메서드를 선언한 BookInformation interface를 선언해줍니다.
public interface BookRepository extends JpaRepository<Book, Long> { ... @Query(value = "select b.name as name, b.category as category from Book b") List<BookNameAndCategory> findBookNameAndCategory(); }
Book Entity를 나타내는 b의 필드 중 name과 category를 조회할 수 있도록 해주고 as를 통해 조회한 쿼리의 컬럼 명을 지정해줍니다.
반환 타입을 BookNameAndCategory로 해주면 getName()이나 getCategory() 메서드를 통해 해당 값을 조회할 수 있습니다.
2) class 활용
@Data @NoArgsConstructor @AllArgsConstructor public class BookInformation { private String name; private String category; }
두 개의 컬럼 name과 category를 필드로 선언한 BookInformation 구체 클래스를 만들어줍니다.
public interface BookRepository extends JpaRepository<Book, Long> { ... @Query(value = "select new {BookInformation 풀 패키지명}.BookInformation(b.name as name), b.category as category from Book b") List<BookNameAndCategory> findBookNameAndCategory(); }
JPQL 문 내부에서 new를 통해 BookInformation 객체를 새로 만들어줍니다. 이 때 @Query 문의 value는 String이기 때문에 BookInformation의 풀 패키지 명으로 써주어야지만 됩니다.
3) 자료구조 활용
public interface BookRepository extends JpaRepository<Book, Long> { ... @Query(value = "select b.name as name, b.category as category from Book b") List<tuple> findBookNameAndCategory(); }
위와 같이 메서드를 선언해 사용한다면 첫 번째 값이 name, 두 번째 값이 category가 들어오게 됩니다.
@Query를 사용하는 목적은 아래 두가지로 설명할 수 있습니다.
1. 메서드 이름이 너무 길어져 가독성이 떨어질 경우
2. 특정 컬럼의 값만 조회하고 싶을 경우 (interface 활용, dto 활용)
Native Query
@Query 어노테이션에서 nativeQuery 속성을 true로 해주면 native query를 작성할 수 있습니다.
public interface BookRepository extends JpaRepository<Book, Long> { ... @Query(value = "select * from book", nativeQuery = true) List<Book> findAllCustom(); }
native query의 경우 JPQL처럼 쿼리문 내에서 Entity를 사용하지 못합니다. 위의 쿼리문 내의 book 또한 Entity가 아니기 때문에 table이름인 소문자 book이 들어간 것을 알 수 있습니다.
기본적으로 제공되는 쿼리메서드인 findAll()광 findAllCustom()의 다른점은
findAll과 같은 경우 JPQL 쿼리로, 모든 컬럼에 alias가 생기게 됩니다.
findAllCustom과 같은 경우 value내에 적힌 쿼리문만으로 동작을 하는 것을 확인할 수 있습니다.
Hibernate: // findAll select book0_.id as id1_1_, book0_.category as category2_1_, book0_.name as name3_1_ from book book0_ Hibernate: //findAllCustom select * from book
위와 같이 native query의 경우 실제 value에 적힌 쿼리문만으로 동작하기 때문에 Entity에 작성한 @Where 어노테이션 또한 포함되지 않게 됩니다.
native query는 db에서 사용하는 sql 쿼리를 그대로 사용하게 됩니다. JPQL과 다르게 dialect를 활용하지 않기 때문에 특정 DB에 의존된 쿼리를 만들게 됩니다.
🔥가능한 문제
native query는 실제 사용 DB와 테스트용 DB로 h2를 둘 경우 문제가 생길 수 있습니다. h2 DB를 사용할 경우 특정 DB를 사용할 때의 호환 모드를 제공하고 있지만 몇 가지 부분에서 차이가 발생하기 때문에 일부 특수한 경우에 테스트 환경에서 오작동하는 경우가 있습니다.
그렇다면 native query를 사용하는 이유는?..
native query를 사용하는 이유
1️⃣ 성능 문제 해결
deleteAll과 같은 경우 findAll을 통해 모든 데이터를 찾아오고 데이터 하나하나 지우고 deleteAllInBatch는 모두 한 번에 제거한다는 차이점을 가지고 있습니다.
delete와 같은 경우 deleteAllInBatch를 제공해줌으로써 성능 문제를 해결할 수 있었습니다.
하지만, update 쿼리의 경우 하나하나 조회를 하고 처리할 수 밖에 없습니다. 이 때 native query를 사용하면 성능 문제를 해결할 수 있습니다.
@Test void test(){ List<Book> books = bookRepository.findAll(); for(Book book : books){ book.setCategory("changed category"); } bookRepository.saveAll(books); }
위와 같이 모든 book 데이터를 update하게 될 경우 모든 Book Entity의 id값을 통해 하나하나 업데이트하게 됩니다. 이와 같은 경우 대용량의 데이터를 처리하게 될 경우 성능 상에 문제가 생길 수 있습니다.
이럴 때, native query를 통해 성능 문제를 해결할 수 있습니다.
@Transactional @Modifing @Query(value = "update book set category = 'changed category'", nativeQuery=true) int updateCategories();
위와 같은 메서드를 통해 모든 데이터를 한 번의 쿼리문으로 update 할 수 있습니다.
dml의 경우 @Modifing을 통해 dml문임을 명시해줍니다. 이 때 return 되는 값을 int나 long으로 하게 되면, affected rows 개수를 반환합니다.
또한, native query를 실행할 경우 Transaction으로 직접 묶어주어야 되기 때문에 메서드에 @Transactional 어노테이션을 선언해주었습니다.
2️⃣ JPA에서 제공하지 않는 기능을 사용할 경우
일반적인 JPA에서 사용할 수 없는 특수한 쿼리를 사용할 경우 native query를 사용합니다.
Jpa를 사용한다면, Entity에 Enum 타입으로 선언된 컬럼의 경우 DB에 무엇이 저장되든 실제로는 Entity에 매핑되서 조회되기 때문에 실제 DB에 무엇이 저장되어 있는지 확인하기 어렵습니다.
이럴 경우 native query를 사용하여 실제 DB에 저장된 값을 확인할 수 있습니다.
show tables와 같은 특수 쿼리도 native query로 조회할 수 있습니다.
Converter
Converter를 통해 쿼리를 통해 가져온 데이터를 커스텀해서 매핑시켜줄 수 있습니다. DB의 레코드를 자바의 객체화 시켜줄 경우 DB데이터와 형식이 다를 경우 알아서 매핑하는 방법입니다.
Enum과 같은 경우에도 알아서 변환해주는 Converter가 내장되어 있기 때문에 사용이 가능한 것입니다.
다른 시스템과 연동할 경우 내가 원하지 않은 형태의 데이터가 들어오는 경우가 있습니다. 이 때, 이 데이터를 변환해서 원하는 형태로 사용할 수 있도록 합니다.
@Entity publi class Book{ ... priavte int status public boolean isDisplayed(){ return status == 200; } }
status 값을 그대로 사용하기 보다는, 좀 더 의미 있는 객체로 변환해보도록 하겠습니다.
BookStatus
@Data public class BookStatus{ private int code; private String description; public BookStatus(int code){ this.code = code; this.description = parseDescription(code); } public boolean isDisplayed(){ return code == 200; } private String parseDescription(int code) { if (code == 100){ ... } }
BookStatus는 Book Entity에 들어갈 속성 중 하나입니다.
code와 description 속성을 가지고 있고 code 값에 따라 description이 지정되게 됩니다.
Book
@Entity publi class Book{ ... @Convert(converter = BookStatusConverter.class) priavte BookStatus status; }
BookStatus 속성을 가지게 됩니다. @Convert 어노테이션을 통해 BookStatus는 DB에 저장할 때 어떻게 바꿀 것인지, DB에서 조회할 경우 어떤 객체로 바꾸어줄 것인지 알려주는 Converter를 지정해줍니다.
BookStatusConverter
@Converter public class BookStatusConverter implements AttributeConverter<BookStatus, Integer>{ @Override public Integer convertToDatabaseColumn(BookStatus attribute) { return attribute.getCode(); } @Override public BookStatus convertToEntityAttribute(Integer dbData) { return dbData != null ? new BookStatus(dbData) : null; } }
AttributeConverter는 java에서 제공하는 converter interface입니다. 첫 번째 제네릭에는 Entity 속성이, 두 번째 제네릭은 DB의 컬럼 타입입니다.
구현해야하는 메서드는 이름에서 알 수 있듯이 순서대로 Entity의 속성을 DB 컬럼 타입으로 바꾸는 것과 DB 컬럼 타입의 값을 Entity의 속성으로 바꾸어주는 역할을 합니다.
Entity Converter에서 나타나는 NullPointException과 같은 경우 DB에 접근된 로직에서 발생하는 것이기 때문에 최대한 오류가 나지 않도록 막는 것이 좋습니다. BookStatus의 경우 nullable하기 때문에 별도로 null일 경우를 처리해줬습니다.
위와 같이 처리할 경우 DB에는 숫자 상태값만 저장이 되고 이를 JPA를 통해 조회를 할 경우에는 BookStatus객체로 변환된 것을 알 수 있습니다.
🔥주의할 점
위와 같은 Converter를 만들어서 사용할 때 주의할 점이 있습니다. DB->객체, 객체->DB로 변환을 해주는 두 개의 메서드 중 한 가지만 사용한다고 해서 한 메서드만 제대로 정의하고 다른 메서드는 null을 반환하는 식으로 정의해서 사용하면 안됩니다.
Jpa는 자동으로 영속성을 관리해주기 때문에 위와 같이 하나의 메서드만 제대로 정의해서 사용할 경우 영속성 컨텍스트에 의해 의도치 않게 동작할 수 있습니다.
예시)
만약 DB에서 값을 조회하는 경우만 필요하고 DB에 다시 저장하지 않는 데이터가 있다고 가정합니다. DB->객체를 변환해주는 메서드만 잘 정의하고 다른 메서드는 null을 반환하도록 남겨둔다고 가정해도록 하겠습니다.
한 트랜잭션이 끝나는 시점에 Entity 값 중에서 변경된 내용이 있을 경우 영속성 컨텍스트는 이를 DB에 반영하게 됩니다.
@Transaction이 선언된 메서드에서 단순히 조회만 하더라도 객체->DB Converter메서드가 제대로 구현되지 않았기 때문에 영속성 컨텍스트 입장에서는 조회한 값과 DB와 확인하는 값이 바뀌었다고 판단하고 제대로 구현되지 않은 Converter를 통해 변환된 값을 DB에 반영하게 됩니다.
그러므로 Converter를 구현하게 된다면, 필요하지 않더라도 구현해야할 변환 메서드를 모두 구현을 해줘야 합니다.
autoApply
autoApply를 선언한 Converter는 첫 번째 제네릭에 선언한 Entity 속성을 객체 타입으로 하는 Entity 속성에 자동으로 converter가 지정이 되는 것을 말합니다.
BookStatusConverter
@Converter(autoApply = true) public class BookStatusConverter implements AttributeConverter<BookStatus, Integer>{ ...
Book
@Entity publi class Book{ ... priavte BookStatus status; }
위와 같이 BookStatus에 Converter를 지정해주지 않아도 자동으로 지정됩니다.
autoApply를 적용할 경우 해당 객체 타입을 갖는 Entity 속성들은 모두 Converter가 적용되기 때문에 유의해서 사용해야 합니다.
반응형LIST'백엔드' 카테고리의 다른 글
Spring Validation (0) 2023.09.13 Jpa Embedded (0) 2023.09.06 JPA Cascade, OrphanRemoval (0) 2023.09.04 Spring IOC, DI, AOP (0) 2023.08.26 JPA Transaction (0) 2023.08.25