8 Spring Data JPA 활용 [Spring Boot]

JuJaeng2·2023년 12월 18일

이번에는 Spring Data JPA에서 제공하는 기능들에 대해서 알아보도록 할 것이다. 이전 글에서 공부했듯이 Spring Data JPA는 ORM 표준 기술 JPA를 쉽게 사용할 수 있도록 제공해주는 기술이다.

✅ JPQL

JPA Query Language

JPQL은 JPA에서 사용할 수 있는 쿼리를 의미한다. JPQL은 엔티티 객체를 대상으로 수행하는 쿼리이기 때문에 매핑된 엔티티으 이름과 필드의 이름을 사용한다.

예를 들어 Product 엔티티에서 JPQL을 사용하면 다음과 같다

SELET p FROM Product p WHERE p.number = ?1;
Product : dpsxlxl xkdlq
p.number : 엔티티 속성

✅ 쿼리 메서드

레퍼지토리에서 JpaRepository를 상속받음으로서 다양한 CRUD 메서드를 제공받게 된다. 하지만 기본으로 제공되는 메서드들은 식별자 기반으로 생성되기 떄문에 별도의 메서드를 정의해서 사용하는 경우가 빈번히 발생한다고 한다.

이때 필요한게 쿼리 메서드이다.

쿼리 메서드 생성

구성

  • 주제 (subject)
  • 서술어 (predicate) : 검색 및 정렬 조건을 지정
    'By' : 서술어의 시작을 나태내는 구분자 역할

AND, OR를 사용해 조건을 확장할수도 있다.

List<Person> findByLastnameAndEmail(String lastName, String email)

find : 주제
By : 서술어의 시작
LastnameAndEmail : 검색 및 정렬조건(여기서는 검색 조건)
And : 조건 확장을 위함

주제(Subject) 키워드

'...'으로 표시한 영역에는 도메인(엔티티)을 표현할 수 있다.

조회하는 기능

  • find...By, read...By, get...By, query...By, search...By, stream...By
Opional<Product> findByNumber(Long number);
List<Product> findAllByName(String name);
Product queryByNumber(Long number);

특정 데이터가 존재하는지 확인

  • exist...By
boolean existByNumber(Long number);

조회 쿼리 수행 후 결과로 나온 레코드의 개수 리턴

  • count...By
long countByName(String name);

삭제 쿼리 수행

  • 리턴타입 : 1. 없는경우, 2. 삭제한 횟수
  • delete...By, remove...By
void deleteByNumber(Long number);
long removeByName(String name);

조회된 결괏값의 개수 제한

  • 한 번의 동작으로 여러 건을 조회할 때 사용
  • 단건일 경우 <number>생략
    -...First<number>..., ...Top<number>...
  • number에 들어간 숫자가 조회할 데이터의 개수이다.
List<Product> findFirst5ByName(String name);
List<Product> findTop10ByName(Sring name)

조건자 키워드

서술어(Predicate) 부분에서 사용할 수 있는 조건자 키워드를 알아본다.

Is

  • 값의 일치를 조건으로 사용하는 키워드
  • Euqlas와 동일한 기능
Product findByNumberIs(Long number);
Product findByNumberEquals(Long number);

(Is)Not

  • 값의 불일치를 조건으로 사용하는 키워드
  • Is 생략 가능
Product findByNumberIsNot(Long number);
Product findByNubmerNot(Long number);

(Is)Null, (Is)NotNull

  • 값이 null인지 검사하는 키워드
List<Proudct> findByUpdateAtNull();
List<Proudct> findByUpdateAtIsNull();
List<Proudct> findByUpdateAtNotNull();
List<Proudct> findByUpdateAtIsNotNull();

(Is)True, (Is)False

  • boolean 타입으로 지정된 칼럼값을 확인하는 키워드
Product findByisActiceTrue();
Product findByisActiceIsTrue();
Product findByisActiceFalse();
Product findByisActiceIsFalse();

And, Or

  • 여러 조건을 묶을 때 사용
Product findByNumberAndName(Long number, String name);
Product findByNumberOrName(Long number, String name);

(Is)GreaterThan, (Is)LessThan, (Is)Between

  • 숫자, dateTime컬럼을 대상으로 한 비교 연산에 사용하는 키워드
  • GreaterThane : 초과의 개념
  • LessThan : 미만의 개념
  • Between : 경계값
// Is는 모두 생략했다.
List<Product> findByPriceGreaterThan(Long price);
List<Product> findByPriceLessThan(Long price);
List<Product> findByPriceBetweenThan(Long lowerPrice, Long highPrice);

StartingWith(= StartsWith), EndingWith(=EndsWith), Containing(Contains), Like

  • 컬럼값에서 일부 일치 여부를 확인하는 키워드
  • SQL의 '%'키워드와 동일한 역할
  • Startingwith : 문자열의 앞
  • EndingWith : 문자열의 끝
  • Containing : 문자열의 양 끝
  • Like : 전달하는 값에 %를 명시적으로 입력해야 한다.
List<Product> findByNameLike(String name);
List<Product> findByNameContians(String name);
List<Product> findByNameStartingWith(String name);
List<Product> findByNameEndsWith(String name);


// Like를 사용할 경우
productRepostiroy.findByNameLike("%홍길동%");
// 이런식으로 %를 어느위치에 사용할지를 명시해야한다.

✅ 정렬과 페이징 처리

지금까지 설명한 쿼리 메스드를 사용해서 정렬과 페이징 처리를 할 수 있다.

정렬

SQL의 ORDER BY 구문을 동일하게 사용한다.

단일 정렬기준

// 오름차순
List<Product> findByNameOrderByNumberAsc(String name);
// 내림차순
List<Product> findByNameOrderByNumberDesc(String name);

여러 정렬기준

List<Product> findByNameOrderByPriceAscStockDesc(String name);
  • Price를 기준으로 오름차순 정렬을 하고 Stock을 기준으로 내림차순 정렬

이렇게 메서드의 이름에 정렬 키워드를 삽입하여 사용하면 길이기 길어져서 가독성이 떨어질 수 있다.
이를 해결하기 위한 매개변수를 활용해 정렬을 하는 방법이 있다.
=> Sort 변수를 활용한다.

// 메소드
List<Product> findByName(String name, Sort sort);

// 사용방법
productRepository.findByName("펜", Sort.by(Order.asc("price"), Order.desc("stock")));

이렇게 매개변수를 사용하면 메소드이름에 키워드를 삽입했던것과 동일한 결과를 얻을 수 있다.

페이징 처리

페이징이란?
데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것
웹 페이지에서 각 페이지를 구분해서 데이터를 제공할 때 그에 맞게 데이터를 용청하는 것이라고 생각하면 된다.

JPA 에서는 Page, Pageable을 사용하여 페징 처리를 한다.

// 메서드 작성
Page<Product> findByName(String name, Pageable pageable);

// 메서드 호출 방법
Pagea<Product> productPage = productRepository.findByName("펜", PageRequest.of(0,2));

// 출력
System.out.println(prdocutPage.getContent());
  • Pageable 파라미터를 전달하기 위해 Pageable의 구현체인 PageRequest 클래스를 사용했다.
  • PageRequest.of(0,2) : 페이지 0번부터 시작하고, 페이지당 개수를 2개로 설정
  • .of() : 오버로딩 되어있다.
  • 출력은 배열 형태로 출력된다.

✅ @Query 어노테이션

직접 JPQL 작성하는 방법

@Query("SELECT p FROM Prdocut p AS WHERE p.name=1?")
List<Product> findByName(String name);

파라미터의 순서가 다를 경우

@Query 어노테이션을 사용해 JPQL 형식의 쿼리문 작성
where절에 1은 처 번째 파라미터를 의미한다. 파라미터의 순서가 바뀌면 오류가 발생할 수 있으므로 @Param 어노테이션을 사용하는 것이 좋다. 그러면 코드의 가독성이 높아지고 유지보수가 수월해질수 있다.

@Query("SELECT p FROM Prdocut p AS WHERE p.name= :name")
List<Product> findByName(@Param("name") String name);

원하는 컬럼의 값만 추출하기

@Query("SELECT p.name, p.price, p.stock FROM Prdocut p AS WHERE p.name= :name")
List<Object[]> findByName(@Param("name") String name);

이때 리턴 타입은 Object배열의 리스트 형태로 리턴타입을 지정해야 한다.


✅ QueryDSL

메서드의 이름을 기반으로 생성하는 JPQL의 한계는 @Query 어노테이션을 통한 직접 작성으로 어느정도 해소할 수 있다. 하지만 이는 직접 문자열을 입력하는 것이기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있다. 실제 운영 환경에 애플리케이션을 배포하고 나서 오류가 발견되는 리스크를 유발한다.
이런 문제를 해결하기 위해 QueryDSL을 사용한다.

QueryDSL은 코드로 쿼리를 작성할 수 있도록 도와준다.

장점

  • IDE가 제공하는 코드 자동 완성 기능을 사용 가능
  • 문법적으로 잘못된 쿼리를 허용하지 않음 => 정상적으로 활용된 QueryDSL은 발생되는 문법 오류 없음
  • 고정된 SQL 쿼리를 작성하지 않기 떄문에 동적으로 쿼리 생성 가능
  • 가독성 및 생산성 향상
  • 도메인 타입과 프로퍼티를 안전하게 참조할 수 있음

✅ JPA Auditing

JPA에서 Audit는 '감시하다'라는 뜻이다.
각 데이터마다 '누가', '언제' 데이터를 생성했고 변경했는지 감시한다는 의미로 사용된다.
'생성일자', '변경 일자'같은 공통적인 필드를 반복해서 주입하는 번거로움을 없애고 자동으로 넣어주는 기능을 제공한다.

많이 사용되는 필드

  • 생성 주체
  • 생성 일자
  • 변경 주체
  • 변경 일자

JPA Auditing 기능 활성화

@EnableJpaAuditing 어노테이션을 추가하면 된다.

@SpringBootApplication
@EnableJpaAuditing
public class AdvancedJpaApplication{
	public static void main(String[] args){
    	SpringApplication.run(JpaApplication.class, args);
    }
}

위 코드처럼 어노테이션을 추가하면 정상작동 하지만 테스트 코드를 작성하고 애플리케이션을 테스트하는 일부 상황에서 오류가 발생할 수 있다.
따라서 별도의 Configuration 클래스를 생성해서 애플리케이션 클래스의 기능과 분리해서 활성화 하는것이 권장된다고 한다.

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration{
	
}

BaseEntity 만들기

각 엔티티에 공통으로 들어가게 되는 컬름(필드)을 빼서 BaseEntity클래스를 만들어야 한다.

@Getter
@Setter
@ToString
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity{
	
    @CreatedDate
    @Column(updateable= false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
}

[ 어노테이션 설명 ]

  • @MappedSuperclass : JPA의 엔티티 클래스가 상속받을 경우 자식 클래스에게 매핑 정보 전달
  • @EntityListeners : 엔티티를 데이터베이스에 적용하기 전후로 콜백을 요청할 수 있게 하는 어노테이션
  • AuditingEntityListener : 엔티티의 Auditing 정보를 주입하는 JPA 엔티티 리스너 클래스
  • @CreatedDate : 데이터 생성 날짜를 자동으로 주입
  • @LastModifiedDate : 데이터 수정 날짜를 자동으로 주입
@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualAndHashCode(callSuper = true)
@Table(name = "product")
public class Product extends BaseEntity{
	
    @Id
    @GeneratedValue(Strategy = GenerationType.IDENTITY)
    private Lonag number;
    
    @Column(nulable = false;
    private String name;
    
    @Column(nulable = false;
    private Integer price;
    
    @Column(nulable = false;
    private Integer stock;
    
}

@ToString, @EqualsAndHashCode 어노테이션의 callSuper 속성은 부모 클래스의 필드를 포함하는 역할을 수행한다.

이렇게 되면 매번 LocalDateTime.now() 메서드를 사용해서 시간을 주입하지 않아도 자동으로 생성된다.

추가로 JPA Auditing 기능에 @CreatedBy, @ModifiedBy 어노테이션도 존재한다. 누가 엔티티를 생성했고 수정했느지 자동으로 값을 주입하는 어노테이션이다. 이기능을 사용하려면 AuditorAware를 스프링 빈으로 등록해야한다.

마무리 😁

이번에는 JPQL을 메서드로 사용하는 방법과 @Query어노테이션을 사용하여 직접사용하는 방법을 알아보았다. 그리고 마지막에는 JPA Auditing을 사용하여 중복코드를 줄일 수 있는 방법도 알아보았다. 지금까지 진행한 프로젝트들에 JPQL을 사용해 왔었는데 이렇게 다양한 방법이 있는지 몰랐다. 평소에는 findById(), findBNumber() 등의 정말 기본적인 것들을 조합해서 사용해 왔다. 쿼리 메서드를 생성하는 규칙을 알게되어서 좀 더 다양한 메서드를 만들수 있을것 같고 지금까지 기본적인 메서드를 조합했던 것들을 하나의 메서드로 변경할 수 있어 데이터베이스의 부담을 줄일 수 있을 것일고 생각했다.
또한 마지막에 JPA Auditing은 너무 필요했던 기술이었다. 매번 Entity를 작성할 때 마다 "코딩은 반복이 일상인가보다..." 하는 생각을 해왔는데 이제 어느정도의 중복 코드는 대체할 수 있게 되었다. 많은사람들이 사용했겠지만 나는 이번에 처음 알게 되었고 빨리 적용을 해보고 싶다는 생각이든다.

북스터디 절반정도 진행했는데 크게 느낀점이 있다. 지금까지 검색으로만 공부를 해오다 보니 구현방법은 알아도 원리나 조금 더 이론적인 부분은 알기 힘들고 항상 찝찝함이 남았다. 그런데 책을 읽어보니 중간중간 비어있던 공간을 채울 수 있었고 이해가 가지않던 동작방법도 채워줄 수 있었다. 이 맛에 개발공부하는가보다 싶다.

profile
다 잘하고 싶은 개발자

0개의 댓글