2. JPA

Veloger·2023년 1월 8일
0

2. JPA

1.1 JPA란?

자바를 사용할 때, SQL의 문제점

  1. 자바 객체를 SQL로 DB를 관리하고, DB를 다시 SQL을 통해 자바 객체로 반환하는 과정이 귀찮음.
  2. 객체와 RDBMS 간의 패러다임의 불일치

RDBMS와 객체지향 간의 중간에서 2개를 매핑하는 역할ORM이라고 한다.
JPA는 이런 ORM의 표준 명세로 자바에서 제공하는 API이다.
JPA는 인터페이스이고, 이를 구현한 구현체가 대표적으로 Hibernate 등이 있다.

[ JPA 장점 ]

1. 특정 DB에 종속되지 않음
- 설정 파일에서 사용할 DB 변경 가능

2. 객체지향 프로그래밍

3. 생산성 향상
- DB에 새로운 칼럼 추가 시, DTO 클래스의 필드도 모두 변경해야 함.
- JPA는 매핑 클래스에 필드만 추가하면 끝!
( DAO : DB의 data에 접근하기 위한 객체 )
( DTO : 계층 간 데이터 교환을 하기 위한 객체 )

[ JPA 단점 ]

1. 복잡한 쿼리 처리
- 복잡하면 SQL 사용 권장
- 이를 보완한 JPQL이 존재

2. 성능 저하 위험

3. 학습 시간

1.2 JPA 동작 방식

JPA 동작 방식

Entity

  • 데이터베이스의 테이블에 대응하는 클래스
  • DB에 item 테이블 만들고, Item.java에 @Entity 붙이면 해당 클래스가 엔티티가 됨.

Entity Manager Factory

  • Entity Manager 인스턴스를 관리하는 주체
  • 실행시, 한 개만 만들어짐.

Entity Manager

  • 영속성 컨텍스트에 접근하여 Entity에 대한 DB 작업을 제공
  • DB에 접근하기 위한 메소드를 제공
메소드내용
find()영속성 컨텍스트에 Entity 검색 후, 없으면 DB에서 해당 데이터 찾아 영속성 컨텍스트에 저장
persist()Entity를 영속성 컨텍스트에 저장
remove()Entity 클래스를 영속성 컨텍스트에서 삭제
flush()영속성 컨텍스트에 저장된 내용을 DB에 반영

Persistence Context

엔티티 생명 주기

[ 생명주기 용어 설명 ]

생명주기내용
newPC와 관련 없는 상태
managedEntity가 PC에 저장되어 관리되는 상태 / 커밋 시점에 DB 반영
detachedPC에 저장되었다가 분리된 상태
removedPC와 DB에서 삭제된 상태

예시

Item item = new Item(); // PC와 관련없음
item.setItemNm("테스트 상품");

EntityManager em = entityManagerFactory.createEntityManager();
// EntityManager 생성


EntityTransaction transaction = em.getTransaction();
// 데이터 변경 시, 무결성 위해 트랜잭션 필수!

em.persist(item);
// PC에 저장된 상태. 아직 SQL X

transaction.commit(); // 트랜잭션을 DB에 반영

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

영속성 컨텍스트 사용 시 이점

  1. 1차 캐시
    영속성 컨텍스트 1차 캐시 구조
  • 위 사진 처럼, find() 시 1차 캐시를 조회함
  1. 동일성 보장
  • 1차 캐시를 조회하기 때문에 하나의 트랜잭션에서 같은 키값으로 조회 가능
  1. 트랜잭션을 지원하는 쓰기 지연
    쓰기 지연 SQL 저장소
  • persist() 시, 쓰기 지연 저장소에도 SQL을 쌓아 커밋시점에 SQL문들을 flush 하면서 DB에 반영됨.
  • 이 때문에 성능면에서 좋음.
  1. 변경 감지
  • 1차 캐시에 처음 DB에서 불러온 Entity의 스냅샷을 가짐.
  • 1차 캐시 저장된 Entity와 스냅샷 사이에 변경이 있으면 UPDATE쓰기 지연 저장소에 보관함.
  • 그리고 커밋 시점에 해당 변경을 자동으로 반영

다음 챕터에선 본격적으로 프로젝트를 만들어보자



2. 프로젝트 생성하기

application.properties 설정

server.port = 80

#MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shop?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1254mlm

#실행되는 쿼리 콘솔 출력
spring.jpa.properties.hibernate.shop_sql=true

#콘솔창에 출력되는 쿼리를 가독성이 좋게 포맷팅
spring.jpa.properties.hibernate.format_sql=true

#쿼리에 물음표로 출력되는 바인드 파라미터 출력
logging.level.org.hibernate.type.descriptor.sql=trace

spring.jpa.hibernate.ddl-auto=create  // DB 초기화 전략
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect // SQL 방언 설정

SQL 방언

SQL은 표존 SQL과 DBMS 벤더에서 제공하는 SQL이 존재.
각 공급업체에서 만든 SQL을 방언(Dialect)이라고 함.

DB 초기화 전략 - DDL AUTO 옵션

  • 애플리케이션 구동 시 JPA의 DB 초기화 전략을 설정 가능.
  • 총 5가지의 옵션을 제공한다
옵션내용
none사용하지 않음
create기존 테이블 삭제 후 테이블 생성
create-drop기존 테이블 삭제 후 테이블 생성. 종료 시점에 테이블 삭제
update변경된 스키마 적요
validate엔티티와 테이블 정상 매핑 확인
  • update는 삭제 시 문제 발생을 고려해, 칼럼 추가만 가능
  • 초반에는 create, update 익숙해지고, 추후 validate 설정
  • 스테이징, 운영환경에선 create, create-drop, update 사용 금지
  • 스테이징, 운영환경에선 none이나 validate 사용.

스테이징 환경이란?

운영 환경은 실제 서비스를 운영하는 환경.
운영환경과 거의 동일한 환경 구성하여, 배포하기 전 여러 가지 기능을 검증하는 환경



3. 상품 엔티티 설계하기

엔티티를 만들기 위해 테이블에 어떤 데이터가 저장될지 설계해야함
그전에 엔티티 매핑 관련 어노테이션을 알아보자

엔티티 매핑 관련 어노테이션

@Lob에 대해

https://javaoop.tistory.com/73

@Column 속성

DDL이란?

테이블, 스키마, 인덱스, 뷰, 도메인을 정의, 변경, 제거할 때 사용하는 용어.
( CREATE, DROP 등이 이에 해당 )
https://cbw1030.tistory.com/71

@GeneratedValue

  • Entity 클래스에는 기본키가 있어야 한다.
  • 기본적으로 @Id 를 통해 기본키로 사용할 칼럼 설정.
  • @GeneratedValue를 통해서도 기본키 생성 전략을 설정.
  • 총 4가지 방법이 존재
생성 전략내용
GenerationType.AUTO (default)JPA 구현체가 자동으로 생성 전략 결정
GenerationType.IDENTITY기본키 생성을 DB에 위임 (MySQL은 AUTO_INCREMENT로 기본키 생성)
GenerationType.SEQUENCEDB 시퀀스 오브젝트를 이용해 기본키 생성. @SqeuenceGenerator로 시퀀스 등록 필요
GenerationType.TABLE키 생성요 테이블 사용. @TableGenerator 필요

기본키와 DB 시퀀스 오브젝트의 의미

기본키 : DB에서 조회시 구분할 수 있는 유일한 기준
시퀀스 : 순차적으로 증가하는 값을 반환해주는 DB 객체

예시

-> com.shop.entity.item.java

@Entity
@Table(name="item")
@Getter
@Setter
@ToString
public class Item {
    @Id
    @Column(name="item_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;       // 상품 코드

    @Column(nullable = false, length = 50)
    private String itemNm; // 상품명

    @Column(name="price", nullable = false)
    private int price;     // 가격

    @Column(nullable = false)
    private int stockNumber; // 재고 수량

    @Lob // BLOB, CLOB 타입 매핑
    @Column(nullable = false)
    private String itemDetail; // 상품 상세 설명

    @Enumerated(EnumType.STRING) // enum 타입 매핑
    private ItemSellStatus itemSellStatus; // 상품 판매 상태 (enum 타입 클래스)

    private LocalDateTime regTime;   // 등록 시간

    private LocalDateTime updateTime; // 수정 시간
}
EnumType.ORDINAL : enum 순서 값을 DB에 저장 EnumType.STRING : enum 이름을 DB에 저장
enum ItemSellStatus {

    SELL, SOLDOUT

}


4. Repository 설계

  • EntityManager를 이용해 엔티티 저장하지 않고, Spring Data JPA에선 EntityManager를 직접 설정하지 않아도 된다.
  • 그대신 DAO(Data Access Object)의 역할을 하는 Repository 인터페이스를 설계한 후 사용하면 된다.

-> com.shop.repository.ItemRepository.java

public interface ItemRepository extends JpaRepository<Item, Long> { }
  • JPARepository는 CRUD 및 페이징 처리를 위한 메소드를 제공.
메소드기능
save(S entity)엔티티 저장 및 수정
void delete(T entity)엔티티 삭제
count()엔티티 총 개수 반환
Iterable모든 엔티티 조회

h2DBMS를 이용해서 테스트 코드를 작성해보자.
테스트 코드용 프로퍼티 파일을 만들어 준다.

-> application-test.properties

# Datasource 설정
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:test
spring.datasource.username=sa
spring.datasource.password=

# H2 DB 방언 설정
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

직접적인 테스트 코드를 만든다.

-> com.shop.repository.ItemRepositoryTest.java

@SpringBootTest // 실제 애플리케이션 구동 할 때처럼 모든 빈을 IoC 컨테이너에 등록
@TestPropertySource(locations="classpath:application-test.properties") // 기본 프로퍼티보다 우선적으로 설정하게 함
class ItemRepositoryTest {

    @Autowired
    ItemRepository itemRepository;

    @Test
    @DisplayName("상품 저장 테스트")
    public void createItemTest(){
        Item item = new Item();
        item.setItemNm("테스트 상품");
        item.setPrice(10000);
        item.setItemDetail("테스트 상품 상세 설명");
        item.setItemSellStatus(ItemSellStatus.SELL);
        item.setStockNumber(100);
        item.setRegTime(LocalDateTime.now());
        item.setUpdateTime(LocalDateTime.now());
        Item savedItem = itemRepository.save(item);
        System.out.println(savedItem.toString());
    }

실행 결과

  • Spring Data JPA는 인터페이스만 작성하면 런타임 시점에 자바의 Dynamic Proxy를 이용해서 객체를 동적으로 생성해줌.
  • 따라서 DAO와 xml에 쿼리를 작성하지 않아도 된다.


5. 쿼리 메소드

Repository 인터페이스에 간단한 네이밍 룰을 이용해 메서드를 작성해서 원하는 쿼리를 실행할 수 있다

[ 규칙 ]

find + (엔티티 이름) + By + 변수이름
  • 엔티티 이름은 생략이 가능하다.

-> ItemRepository의 예시

public interface ItemRepository extends JpaRepository<Item, Long> {

    List<Item> findByItemNm(String itemNm);
  • 이제 해당 메서드를 테스트 코드에서 사용해보자!

-> ItemRepositoryTest.java

// 아이템 리스트 만드는 메서드
public void createItemList(){
        for(int i=1;i<=10;i++){
            Item item = new Item();
            item.setItemNm("테스트 상품" + i);
            item.setPrice(10000 + i);
            item.setItemDetail("테스트 상품 상세 설명" + i);
            item.setItemSellStatus(ItemSellStatus.SELL);
            item.setStockNumber(100); item.setRegTime(LocalDateTime.now());
            item.setUpdateTime(LocalDateTime.now());
            Item savedItem = itemRepository.save(item);
        }
    }

    @Test
    @DisplayName("상품명 조회 테스트")
    public void findByItemNmTest(){
        this.createItemList(); // 1. 아이템들 생성
        List<Item> itemList = itemRepository.findByItemNm("테스트 상품1"); // ItemNm이 테스트 상품1인 아이템을 받기
        for(Item item : itemList){
            System.out.println(item.toString()); // 해당 리스트 출력
        }
    }

실행 결과

Hibernate:
select
    item0_.item_id as item_id1_0_,
    item0_.item_detail as item_det2_0_,
    item0_.item_nm as item_nm3_0_,
    item0_.item_sell_status as item_sel4_0_,
    item0_.price as price5_0_,
    item0_.rec_time as reg_time6_0_,
    item0_.stock_number as stock_nu7_0_,
    item0_.update_time as update_t8_0_,
from
    item item0_
where
    item0_.item_nm=?

binding parameter [1] as [VARCHAR] - [테스트 상품1]

여러 조건 처리하기

  • 여러 개의 조건을 이용한 검색 및 정렬도 가능하다.
  • 이 때에는 JPQL을 이용하기도 한다. (2.6절에서..)

1. OR 조건

// 인터페이스
 List<Item> findByItemNmOrItemDetail(String itemNm, String itemDetail);

// 테스트 코드
    @Test
    @DisplayName("상품명, 상품상세설명 or 테스트")
    public void findByItemNmOrItemDetailTest(){
        this.createItemList();
        List<Item> itemList = itemRepository.findByItemNmOrItemDetail("테스트 상품1", "테스트 상품 상세 설명5");
        for(Item item : itemList){
            System.out.println(item.toString());
        }
    }

2. LessThan 조건

// 인터페이스
List<Item> findByPriceLessThan(Integer price);

// 테스트 코드
@Test
    @DisplayName("가격 LessThan 테스트")
    public void findByPriceLessThanTest(){
        this.createItemList();
        List<Item> itemList = itemRepository.findByPriceLessThan(10005);
        for(Item item : itemList){
            System.out.println(item.toString());
        }
    }

3. 내림차순 정렬

  • 오름차순 정렬 : OrderBy + 속성명 + Asc` 를 이용하면 된다!
  • 내림차순 정렬 : OrderBy + 속성명 + Desc` 를 이용하면 된다!
// 인터페이스
List<Item> findByPriceLessThanOrderByPriceDesc(Integer price); // 내림차순

// 테스트 코드
    @Test
    @DisplayName("가격 내림차순 조회 테스트")
    public void findByPriceLessThanOrderByPriceDesc(){
        this.createItemList();
        List<Item> itemList = itemRepository.findByPriceLessThanOrderByPriceDesc(10005);
        for(Item item : itemList){
            System.out.println(item.toString());
        }
    }


6. @Query

  • JPARepository의 메서드를 이용해서 조회 시, 조건이 많아지면 이름만으론 구분하기 힘들다.
  • 그렇기 때문에 직접 쿼리를 작성할 수 있도록 @Query를 지원한다.
  • @Query는 JPQL을 이용해서 작성해야한다.

JPQL

  • 특정 DB SQL에 의존하지 않음.

예시) ->ItemRepository

    @Query("select i from Item i where i.itemDetail like " + // like 다음에 띄어쓰기 해야됨
            "%:itemDetail% order by i.price desc")
    List<Item> findByItemDetail(@Param("itemDetail") String itemDetail);
  • @Param : 파라미터로 넘어온 값을 JPQL에 들어갈 변수로 지정.
    • like % % 사이에 :itemDetail로 값이 들어감

-> ItemRepositoryTest

    @Test
    @DisplayName("@Query를 이용한 상품 조회 테스트")
    public void findByItemDetailTest(){
        this.createItemList();
        List<Item> itemList = itemRepository.findByItemDetail("테스트 상품 상세 설명");
        for(Item item : itemList){
            System.out.println(item.toString());
        }
    }
  • "테스트 상품 상세 설명"을 포함하는 상품 데이터 10개가 가격이 높은 순부터 조회가 된다.

기존 DB에서 사용하던 쿼리 그대로 사용하기

@QuerynativeQuery속성을 사용.

-> ItemRepository

@Query("select i from Item i where i.itemDetail like " +
        "%:itemDetail% order by i.price desc", nativeQuery = true)
List<Item> findByItemDetail(@Param("itemDetail") String itemDetail);

-> ItemRepositoryTest

    @Test
    @DisplayName("nativeQuery를 이용한 상품 조회 테스트")
    public void findByItemDetailTest(){
        this.createItemList();
        List<Item> itemList = itemRepository.findByItemDetail("테스트 상품 상세 설명");
        for(Item item : itemList){
            System.out.println(item.toString());
        }
    }

실행 결과


7. Querydsl

  • @Query는 JPQL 문법을 문자열로 입력하기 때문에, 컴파일 시점에 에러가 발견되지 않는다.
  • 이를 보완하기 위한 Querydsl를 알아보자.

Querydsl

@Query("select i from Item wheere ...)

위 코드의 JPQL의 문법 중 where를 wheere로 잘못 입력되었다.
이때 Querydsl이 오타 발생시 바로 알려주고, 동적으로 쿼리를 생성해준다!

Querydsl 장점

  • 고정된 SQL문이 아닌 조건에 맞게 동적으로 쿼리 생성
  • 비슷한 쿼리 재사용 가능 및 가독성 향상
  • 문자열이 아닌 자바 소스코드로 작성됨 (컴파일 시 오류 발견 ㅆㄱㄴ)
  • IDE 도움을 받아 자동 완성까지 지원!

Querydsl 설정하기

gradle 버전

JPAQueryFactory로 상품 조회하기

-> ItemRepositoryTest

    @PersistenceContext
    EntityManager em;

     @Test
    @DisplayName("Querydsl 조회 테스트1")
    public void queryDslTest(){
        this.createItemList();
        JPAQueryFactory queryFactory = new JPAQueryFactory(em); // 쿼리를 동적으로 생성
        QItem qItem = QItem.item; // 쿼리 생성위해 자동생성된 QItem 객체 사용
        JPAQuery<Item> query = queryFactory.selectFrom(qItem)
                .where(qItem.itemSellStatus.eq(ItemSellStatus.SELL))
                .where(qItem.itemDetail.like("%" + "테스트 상품 상세 설명" + "%"))
                .orderBy(qItem.price.desc());

        List<Item> itemList = query.fetch(); // 쿼리 결과를 리스트로 반환, fetch(0 시점에 쿼리문 실행

        for(Item item : itemList){
            System.out.println(item.toString());
        }
    }
  • @PersistenceContext : 영속컨텍스트 사용하기 위해 EntityManager 빈을 주입
  • QItem : QueryDSL전용 엔티티 (간단한 쿼리는 (Spring Data)JPA를 사용하고, 동적쿼리는 QueryDSL을 사용하여 개발하곤 합니다.)

JPAQuery 데이터 반화 메소드

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

실행결과

Hibernate:
select
    item0_.item_id as item_id1_0_,
    item0_.item_detail as item_det2_0_,
    item0_.item_nm as item_nm3_0_,
    item0_.item_sell_status as item_sel4_0_,
    item0_.price as price5_0_,
    item0_.rec_time as reg_time6_0_,
    item0_.stock_number as stock_nu7_0_,
    item0_.update_time as update_t8_0_,
from
    item item0_
where
    item0_.item_sell_status=?
    and(
        item-_.item_detail like ? escape '!'
    )
order by
    item0_.price desc

QuerydslPredicateExecutor를 이용한 상품 조회를 해보자

Predicate란?

'이 조건이 맞다'고 판단하는 근거를 함수로 제공하는 것

-> ItemRepository

public interface ItemRepository extends JpaRepository<Item, Long>,
        QuerydslPredicateExecutor<Item> {
  • Pepository에 Predicate를 파라미터로 전달하기 위해 코드 수정

QueryDslPredicateExecutor 인터페이스 제공 메소드

메소드내용
long count(Predicate)조건에 맞는 데이터 총 개수 반환
boolean exists(Predicate)조건에 맞는 데이터 존재 여부 반환
Iterator findAll(Predicate)조건에 맞는 모든 데이터 반환
Page findAll(Predicate, Pageable)조건에 맞는 페이지 데이터 반환
Iterator findAll(Predicate, Sort)조건에 맞는 정렬된 데이터 반환
T findOne(Predicate)조건에 맞는 데이터 1개 반환

-> ItemRepository

public void createItemList2(){
        for(int i=1;i<=5;i++){
            Item item = new Item();
            item.setItemNm("테스트 상품"+i);
            item.setPrice(10000 + i);
            item.setItemDetail("테스트 상품 상세 설명"+i);
            item.setItemSellStatus(ItemSellStatus.SELL);
            item.setStockNumber(100);
            item.setRegTime(LocalDateTime.now());
            item.setUpdateTime(LocalDateTime.now());
            itemRepository.save(item);
        }

        for(int i=6;i<=10;i++){
            Item item = new Item();
            item.setItemNm("테스트 상품"+i);
            item.setPrice(10000 + i);
            item.setItemDetail("테스트 상품 상세 설명"+i);
            item.setItemSellStatus(ItemSellStatus.SOLD_OUT);
            item.setStockNumber(0);
            item.setRegTime(LocalDateTime.now());
            item.setUpdateTime(LocalDateTime.now());
            itemRepository.save(item);
        }
    }

    @Test
    @DisplayName("상품 Querydsl 조회 테스트2")
    public void queryDslTest2(){

        this.createItemList2();

        BooleanBuilder booleanBuilder = new BooleanBuilder(); // 2: 쿼리에 들어갈 조건을 만들어주는 빌더
        QItem item = QItem.item;

        String itemDetail = "테스트 상품 상세 설명";
        int price = 10003;
        String itemSellStat = "SELL";

        booleanBuilder.and(item.itemDetail.like("%" + itemDetail + "%")); // 3: 조회 시 조건
        booleanBuilder.and(item.price.gt(price));

        if(StringUtils.equals(itemSellStat, ItemSellStatus.SELL)){
            booleanBuilder.and(item.itemSellStatus.eq(ItemSellStatus.SELL));
        }

        Pageable pageable = PageRequest.of(0, 5); // 4: 데이터를 페이징해 조회하도록 Pageble 객체 생성
        Page<Item> itemPagingResult =
                itemRepository.findAll(booleanBuilder, pageable); // 5: 조건에 맞는 데이터를 Page 객체로 받아옴.
        System.out.println("total elements : " +
                itemPagingResult. getTotalElements());

        List<Item> resultItemList = itemPagingResult.getContent();
        for(Item resultItem: resultItemList){
            System.out.println(resultItem.toString());
        }
    }
  • createItemList2 : 1~10까진 SELL, 11~20까진 SOLDOUT
  • BooleanBuilder : 동적으로 쿼리를 생성해주면서 조건까지 넣을 꺼임
  • .and : 조회 시 사용할 조건들을 설정
  • Pageable : 페이지를 어떻게 보여줄지 정보를 전달하는 API
  • PageRequest : 몇 페이지, 한 페이지의 사이즈, Sorting 방법(Option)을 가지고, Repository에 Paging을 요청할 때 사용하는 것
    [ 첫번째 인자 : 조회할 페이지 번호, 두번째 : 한 페이지당 조회할 데이터 개수]

실행결과

Hibernate:
select
    item0_.item_id as item_id1_0_,
    item0_.item_detail as item_det2_0_,
    item0_.item_nm as item_nm3_0_,
    item0_.item_sell_status as item_sel4_0_,
    item0_.price as price5_0_,
    item0_.rec_time as reg_time6_0_,
    item0_.stock_number as stock_nu7_0_,
    item0_.update_time as update_t8_0_,
from
    item item0_
where
    (
        item0_.item_detail like ? escape '!'
    )
    and item0_.price>?
    and item0_.item_sell_status=? limit ?

0개의 댓글

관련 채용 정보