Lined Notebook

[백견불여일타 스프링 부트 쇼핑몰] 01. Spring Data JPA

by ymkim

JPA, ORM

  • JPA(Java Persistence API)
    • 자바 ORM 기술에 대한 API 표준
    • Hibernate, EclipseLink, DataNucleus, OpenJPA, TopLink등 존재
  • ORM(Object Relation Mapping)
    • 객체는 객체대로 설계하고 DB는 DB대로 설계한 후에 객체와 DB를 자동 매핑해주는 프레임워크
    • 대표적으로 JPA가 존재함

JPA 사용 시 장점

  1. DB에 종속적이지 않음
    • 애플리케이션 개발을 위해 최초 설계시에 ORACLE을 사용하여 개발했다고 가정한다
    • 만약 ORACLE을 MariaDB로 변경한다면 DB마다 쿼리문이 다르기에 전체적인 수정이 필요하다
    • 따라서 처음 선택한 DB를 변경한다는 것은 쉬운일이 아님
    • 하지만 JPA는 추상화된 데이터 접근 계층을 제공함
      • 설정 파일에 DB만 지정해주면 언제든지 변경이 가능함
  2. 객체지향적 프로그래밍 가능
    • JPA를 사용하면 데이터베이스 설계 중심의 패러다임에서 객체지향적으로 설계가 가능하다
      • 패러다임의 불일치를 극복할 수 있음
    • 조금 더 비즈니스 로직 작성에 집중할 수 있음
  3. 생상성 향상
    • DB 테이블에 새로운 컬럼이 추가되는 경우 DTO도 전부 수정해야 함
      • JPA는 테이블과 매핑된 클래스에 필드만 추가하면 끝
    • SQL을 직접 작성하지 않고 객체를 사용하여 동작하기에 유지보수 관점에도 좋음

JPA 사용 시 단점

  1. 복잡한 쿼리 처리
    • 통계 처리 같은 복잡한 쿼리는 SQL(Mybatis) 사용이 더 나을수 있음
    • JPA에서는 Native SQL을 통해 SQL 사용 가능하지만, 특정 DB에 종속된다는 단점 발생
    • 이를 보완하기 위해 JPQL 지원
  2. 성능 저하 위험
    • 객체간의 매핑 설계를 잘못했을 때 성능 저하 발생 가능
    • 자동 생성 쿼리가 많기에 개발자가 의도하지 않는 쿼리로 인해 성능 저하 발생 가능
  3. 학습 시간
    • 러닝커브가 높음

JPA 동작 방식

  1. 엔티티
    • 데이터베이스의 테이블에 대응하는 클래스 ( 1:1 )
    • @Entity가 붙은 클래스는 JPA가 관리
  2. 엔티티 매니저 팩토리
    • 엔티티 매니저 인스턴스를 관리하는 주체
    • 애플리케이션 실행 시 한 개만 만들어지며 사용자의 요청이 오면 엔티티매니저 팩토리로부터 엔티티 매니저 생성
  3. 엔티티 매니저
    • 영속성 컨텍스트에 접근하여 엔티티에 대한 데이터베이스 작업을 제공
    • 내부적으로 DB 커넥션을 사용하여 DB에 접근
    • 메서드 종류
      • find()
        • 영속성 컨텍스트에서 엔티티를 검색
        • 영속성 컨텍스트에 엔티티가 없을 경우 DB에서 조회 후 영속성 컨텍스트에 저장
      • persist()
        • 영속성 컨테스트에 저장
      • remove()
        • 엔티티 클래스를 영속성 컨텍스트에서 삭제
      • flush()
        • 영속성 컨텍스트에 저장된 내용을 DB에 반영
  4. 영속성 컨텍스트
    • 엔티티를 영구 저장하는 환경
    • 엔티티 매니저를 통해 영속성 컨텍스트에 접근한다

생명주기 내용

비영속(new) new 키워드를 통해 생성된 상태로 영속성 컨테스트와 상관 없는 상태
영속(managed) 1. 엔티티가 영속성 컨텍스트에 저장된 상태로 영속성 컨텍스트에 의해 관리되는 상태
  1. 영속 상태에서 DB에 저장되지 않으며, 트랜잭션 커밋 시점에 DB 반영 | | 준영속 상태(detached) | 영속성 컨텍스트에 저장 되었다가 분리된 상태 | | 삭제 상태(removed) | 영속성 컨텍스트에와 DB에서 삭제된 상태 |
// 01. 영속성 컨텍스트에 저장할 상품 엔티티 하나를 생성
Item item = new Item();
item.setItemNm("테스트 상품");

// 02. 엔티티 매니저 팩토리(엔티티 매니저 관리하는 주체) 로부터 엔티티 매니저 생성
EntityManager em = EntityManagerFactory.createEntityManager(); // 사용자의 요청당 하나의 em 생성

// 03. 엔티티 매니저는 데이터 변경 시 데이터 무결성을 위해 반드시 트랜잭션을 시작해야함
//     여기서의 트랜잭션도 DB 트랜잭션과 같은 의미로 봐도 됨
EntityTranscation transaction = em.getTransaction();
transaction.begin();

// 04. 영속성 컨테스트에 엔티티 저장
em.persist(item);

// 05. 트랜잭션 DB에 반영, 영속성 컨텍스트에 저장된 상품 정보가 DB INSERT 되면서 반영
transaction.commit();

// 06. 자원 반환
em.close();
emf.close();

영속성 컨텍스트 사용 이점

📌 영속성 컨텍스트의 이점 1-2

  1. JPA는 왜 이렇게 영속성 컨텍스트 라는 것을 사용하는 것인가?
    1. 애플리케이션과 DB 사이의 중간 계층을 만들었기 때문이다
  2. 중간 계층이 있으면 뭐가 좋은가?
    1. 1차 캐싱
    2. 동일성 보장
    3. 트랜잭션을 지원하는 쓰기 지연 ( transcation write-behind )
    4. 변경 감지
    5. 지연 로딩

1차 캐시

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction(); // 트랜잭션을 얻는다
tx.begin(); // 트랜잭션 시작 -> DB 트랜잭션 시작

//여기서 가져온 Entity는 DB에서 가져온 값으로, 1차캐시 영역에 저장이 된다
Member a = em.find(Member.class, "member1");

//여기서 가져온 Entity는 DB가 아닌, 1차 캐시에서 값을 가져온다
Member b = em.find(Member.class, "member1");
  • 영속성 컨텍스트 안에 1차 캐시라는 것이 존재함
  • Map<Key, Value> 형태로 저장
  • em.find 메서드 호출 시 영속성 컨텍스트의 1차 캐시를 조회한다. 엔티티가 존재할 경우 해당 엔티티를 반환하고 존재하지 않는다면 DB에서 조회 후 1차 캐시에 저장 후 반환한다

동일성 보장

  • 하나의 트랜잭션에서 같은 키값으로 영속성 컨텍스트에 저장된 엔티티 조회 시 같은 엔티티 조회 보장
  • 바로 1차 캐시에 저장된 엔티티를 조회하기 때문에 가능

트랜잭션을 지원하는 쓰기 지연

  • 영속성 컨텍스트에서는 1차 캐시뿐 아니라 쓰기 지연 SQL 저장소가 존재한다
  • em.persist() 호출 시 1차 캐시에 저장되는 것과 동시에 쓰기 지연 SQL 저장소에 SQL문이 저장됨
  • SQL을 쌓아두고 트랜잭션 커밋 시점에 저장된 SQL문들이 flush 되면서 DB에 반영 됨
  • SQL을 모아서 보내기 때문에 성능적 이점이 있음

변경 감지

  • JPA는 1차 캐시에 DB에서 처음 불러온 엔티티의 스냅샷을 가지고 있다. 그리고 1차 캐시에 저장된 엔티티와 스냅샷을 비교 후 변경 내용이 있다면 UPDATE SQL을 쓰기 지연 SQL 저장소에 담아둔다. 그리고 데이터베이스 커밋 시점에 변경 내용을 자동으로 반영한다. 즉, 따로 update 호출이 필요가 없음
  • 중요한 부분은 JPA가 엔티티의 스냅샷(snapshot)을 가지고 있어 AS-IS, TO-BE 비교를 통해 업데이트 수행한다는 점인 것 같다

상품 엔티티 설계

엔티티 관련 어노테이션 정리

어노테이션 설명 비고

@Entity 클래스를 엔티티로 선언  
@Table 해당 엔티티와 매핑할 테이블 지정  
@Id 테이블의 기본 키에 사용할 속성 지정  
@GeneratedValue 기본키 생성 전략  
@Column 자바 필드와 테이블 컬럼 매핑  
@Lob BLOB, CLOB 타입 매핑  
@CreationTimeStamp insert 시에 시간 자동 저장 hibernate 제공
@UpdateTimeStamp update 시에 시간 자동 저장  
@Enumerated enum 타입 매핑  
@Transient 해당 필드 데이터베이스 매핑 무시  
@Temporal 날짜 타입 매핑  
@CreateDate 엔티티가 생성되어 저장될 때 시간 자동 저장 스프링 제공
@LastModifiedDate 조회한 엔티티의 값을 변경할 때 시간 자동 저장  
  • CLOB
    • 사이즈가 큰 데이터를 외부 파일로 저장하는 위한 데이터 타입
    • 문자형 대용량 파일을 저장하는데 사용하는 데이터 타입
  • BLOB
    • 바이너리 데이터를 DB 외부에 저장하기 위한 데이터 타입
    • 이미지, 사운드, 비디오 같은 멀티미디어 데이터를 다루기 위한 타입

@CreationTimestamp와 @CreatedDate의 선택 - 인프런 | 질문 & 답변

@Colum 옵션

속성 설명 기본값

name 필드 변수와 매핑할 테이블 컬럼명 객체의 필드 이름
unique(DDL) 유니크 제약 조건 설정  
insertable insert 가능 여부 true
updatable update 가능 여부 true
length String 타입의 문자 길이 제약조건 설정  
nullable(DDL) null 값 허용 여부 설정, false 설정 시 DDL 생성 시에 not null 제약 조건 추가  
columnDefinition 데이터베이스 컬럼 정보 직접 기술  

@Colum(columnDefinition = “varchar(5)” default’10’ not null) | | | precision, scale(DDL) | BigDecimal 타입에서 사용(BigInteger 가능) precision은 소수점을 포함한 전체 자리수, scale은 소수점 자리수 Double과 float 타입에는 적용되지 않음

  • 테이블 생성 시 컬럼에는 다양한 조건이 들어간다
  • 위 어노테이션을 사용하면 다양한 조건 처리가 가능
  • DML(Data Manipulation Language)
    • SELECT
    • INSERT
    • DELETE
    • UPDATE
  • DDL(Data Definition Language)
    • 테이블, 스키마, 인덱스, 뷰, 도메인을 정의 및 변경 시 사용
      • CREATE
      • ALTER
      • RENAME
      • TRUNCATE
  • DCL(Data Control Langauge)
    • GRANT
    • REVOKE

@GeneratedValue 옵션

기본키 생성 전략 설명

GenerationType.AUTO (default) JPA 구현체가 자동 생성 전략 결정
GenerationType.IDENTITY 기본키 생성 DB에 위임
MySQL은 AUTO_INCREMENT 전략 사용  
GenerationType.SEQUENCE DB 시퀸스 오브젝트 이용한 전략
@SequenceGenerator 사용 + 시퀸스 등록 필요  
GenerationType.TABLE 키 생성용 테이블 사용. @TableGenerator 필요
  • 기본키 생성 전략
    • 기본키를 생성하는 전략이라 생성하는게 편할 듯
    • DB에서 seq(시퀸스) 값 처럼 사용을 하는 경우가 많음
  • 기본키
    • 기본키는 DB에서 조건을 만족하는 튜플을 찾을 때 다른 튜플들과 유일하게 구별할 수 있도록 기준을 세워두는 속성이다
    • 상품 데이터를 찾을 때 다른 상품들과 구별을 하기 위해 사용
    • 여기서 기본키는 id다

상품 리포지토리 설계

이전에 EntityMangerFactory와 EntityManger를 사용하여 Item 엔티티를 저장한 적이 있었다. Spring Data JPA에서는 위와 같이 EntityManager를 사용하여 코드를 작성하지 않아도 된다.

그렇다면 왜 위와 같은 코드를 작성 하였는가? 그것은 JPA의 기본적인 메커니즘을 이해하기 위함이였고 Spring Data JPA는 Data Access Object의 역할을 하는 리포지토리(인터페이스) 설계하는것만으로도 충분함.

package com.shop.repository;

import com.shop.entity.Item;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ItemRepository extends JpaRepository<Item, Long> {

}
  • JPARepository를 상속받는 ItemRepository 작성
  • JPARepository<T, ID> → 2개의 제네릭 타입을 사용
    • T : 엔티티 타입 클래스
    • ID : 기본키 타입
  • JPARepository는 기본적인 CRUD 및 페이징 처리 기능을 제공
    • <S extends T> save(S entity) : 엔티티 저장 및 수정
    • void delete(T entity) : 엔티티 삭제
    • count() : 엔티티 총 개수 반환
    • Iterable<T> findAll() : 모든 엔티티 조회

Spring Data JPA는 인터페이스만 작성하면 런타임 시점에 자바의 Dynamic Proxy를 이용해 객체를 동적으로 생성해준다

쿼리 메서드

35. JPA-쿼리메서드기능-정리

find + (엔티티 이름) + By + 변수이름
  • JPA에서 @Query 사용 시 기본적인 룰(Rule)이 존재한다

Spring Data JPA - Snippet

Spring Data JPA - Reference Documentation

Keyword Sample JPQL snippet

And findByLastNameAndFirstName … where x.lastName = ?1 and x.firstName = ?2
Or findByLastNameOrFirstName … where x.lastName = ?1 or x.firstName = ?2
Is, Equals findByFirstName  
findByFirstNameIs    
findByFirstNameEquals … where x.firstName = ?1  
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age ≤ ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age ≥ ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull, Null, IsNotNull findByAge(Is)Null … where x.age is null
NotNull findByAge(Is)NotNull … where x.age is not null
Like findByFirstNameLike … where x.firstName like ?1
StartingWith findByFirstNameStartingWith … where x.firstName like ?1 (parameter bound with appended %)
EndingWith findByFirstNameEndingWith … where x.firstName like ?1 (parameter bound with prepended %)
Containing findByFirstNameContaining … where x.firstName like ?1 (parameter bound wrapped %)
OrderBy findByAgeOrderBylastNameDesc … where x.age = ?1 order by x.lastName desc
Not findByLastNameNot … where x.lastName <> ?1
In findByAgeIn … where x.age in ?1
NotIn findByAgeNotIn … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstNameIgnoreCase … where UPPER(x.firstName) = UPPER(?1)

Spring Data JPA의 @Query 어노테이션

쿼리 메서드를 사용해서 한 두개 정도의 조건을 이용하여 상품 데이터를 조회하였다. 하지만 만약 조건이 많아지면 어떻게 되는 것일까? 아마도 인터페이스에 선언된 메서드의 이름이 엄청나게 길어지는 현상이 발생할 것이다. 즉, 간단한 쿼리 처리는 가능하지만 복잡한 쿼리에서의 사용이 그렇게 쉽지는 않다는 의미로 보인다. 이번에는 이러한 부분을 보완하기 위해 @Query 어노테이션을 사용해보자.

  • Spring Data JPA에서 제공하는 @Query 어노테이션을 사용하면 SQL과 유사한 JPQL 이라는 객체지향 쿼리 언어를 통해 복잡한 쿼리(엄청 복잡한 쿼리는 힘듬)도 처리가 가능하다.
  • SQL은 데이터베이스의 테이블을 대상으로 쿼리를 수행, JPQL은 테이블이 아닌 엔티티를 대상으로 쿼리 수행
  • JPQL은 SQL을 추사항하여 사용하기 때문에 데이터베이스 종속적이지가 않음

[Spring JPA] 멀티컬럼 - 쿼리메소드 vs @Query

[Spring Data JPA] 쿼리 메서드 기능 ①

package com.shop.repository;

public interface ItemRepository extends JpaRepository<Item, Long> {

		...

    /*
     * @Query 어노테이션 기반 상세 상품명을 이용하여 상품 조회
     */
    @Query("select i from Item i where i.itemDetail like %:itemDetail% order by i.price desc")
    List<Item> findByItemDetail(@Param("itemDetail") String itemDetail);

}
  • @Query 안에 JPQL 쿼리문 작성
  • @Param 어노테이션 사용하여 %:itemDetail% 자리에 들어갈 파라미터 지정
  • 만약 기존 데이터베이스에 사용하던 쿼리를 그대로 사용하려고 한다면 @Query의 native query 옵션을 사용하면 된다
    • 특정 DB에 종속적이라는 단점 존재

Spring Data JPA QueryDsl

@Query 어노테이션 안에 JPQL 문법으로 문자열을 입력하기 때문에 컴파일 시점에 에러를 발견할 수 없다 이를 보완할 수 있는 방법으로 QueryDSL이 존재한다.

QueryDsl

  • JPQL을 문자열이 아닌 자바 코드로 사용 가능하게 해주는 빌더 API
  • 문자열이 아니기 때문에 컴파일러의 도움을 받을 수 있음
  • 동적으로 쿼리를 생성해줌
// For querydsl : <https://devfunny.tistory.com/844>
buildscript {
	ext {
		querydslVersion = "5.0.0"
	}
}

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.10'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

group = 'com.shop'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	// Spring Boot Data JPA
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

	// Template Engine
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

	// Inner Tomcat, Zetty, Undertow..
	implementation 'org.springframework.boot:spring-boot-starter-web'

	// Lombok
	compileOnly 'org.projectlombok:lombok'

	// Querydsl 라이브러리
	implementation "com.querydsl:querydsl-jpa:${querydslVersion}"
	annotationProcessor "com.querydsl:querydsl-apt:${querydslVersion}" // Querydsl 관련 코드 생성 기능 제공

	runtimeOnly 'com.h2database:h2'
	runtimeOnly 'com.mysql:mysql-connector-j'

	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

//querydsl 추가 시작 (위에 plugin 추가 부분과 맞물림)
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
	jpa = true
	querydslSourcesDir = querydslDir
}

sourceSets {
	// IDE의 소스 폴더에 자동으로 넣어준다.
	main.java.srcDir querydslDir
}

configurations {
	// 컴파일이 될때 같이 수행
	querydsl.extendsFrom compileClasspath
}

compileQuerydsl {
	// Q파일을 생성
	options.annotationProcessorPath = configurations.querydsl
}
@PersistenceContext
EntityManager em; // 1

@Test
@DisplayName("QueryDSL 조회 기능 테스트")
void queryDslTest() throws Exception {
    //given
    this.createSampleItemList();
    JPAQueryFactory queryFactory = new JPAQueryFactory(this.em); // 2
    QItem qItem = QItem.item; // 3

    //when
    JPAQuery<Item> query = queryFactory.selectFrom(qItem) // 4
            .where(qItem.itemSellStatus.eq(ItemSellStatus.SELL))
            .where(qItem.itemDetail.like("%" + "테스트 상품 상세 설명" + "%"))
            .orderBy(qItem.price.desc());

    List<Item> result = query.fetch(); // 5
    System.out.println("result => " + result.toString());

    //then
}
  1. 영속성 컨텍스트를 사용하기 위해 @PersistenceContext 어노테이션 사용하여 EntityManager Bean 주입
  2. JPAQueryFactory를 이요하여 동적 쿼리 생성, 생성자 파라미터로는 EntityManager 넘긴다
  3. QueryDSL을 통해 쿼리를 생성하기 위해 플러그인 통해 자동으로 생성된 QItem 객체 이용
  4. 자바 소스처럼 SQL문 작성
  5. JPAQuery Method 중 하나인 fetch 이용하여 쿼리 결과 리스트로 반환, fetch() 실행 시점에 쿼리문 실행함 JPAQuery Method는 아래 표 참고

메서드 기능

List<T> fetch() 조회 결과 리스트 반환
T fetchOne 조회 대상이 1건인 경우 제네릭으로 지정한 타입 반환
T fetchFirst 조회 대상 중 1건만 반환
Long fetchCount() 조회 대상 개수 반환
QueryResult<T> fetchResults() 조회한 리스트와 전체 개수를 포함한 QueryResults 반환

QueryDslPredicateExecutor 사용

어떠한 조건에 부합하는가?

  1. Predicate란 ‘이 조건이 맞다’고 판단하는 근거를 함수로 제공
  2. ItemRespository에서 QueryDslPredicateExecutor 상속
  3. 실무에서 사용하기 까다롭다는 말이 많음, 일단 뭐 한번 써봅시다 간단히라도

메서드 기능

long count(Predicate) 조건에 맞는 데이터 총 개수 반환
boolean exists(Predicate) 조건에 맞는 데이터 존재 여부 반환
Iterable findAll(Predicate) 조건에 맞는 모든 데이터 반환
Page<T> findAll(Predicate, Pageable) 조건에 맞는 페이지 데이터 반환
Iterable findAll(Predicate, Sort) 조건에 맞는 정렬 데이터 반환
T findOne(Predicate) 조건에 맞는 데이터 1개 반환

블로그의 정보

기록하고, 복기하고

ymkim

활동하기